Java 中 null 值在 JSON 输出时丢失的坑:一次 Object 参数 + Fastjson 多态的血泪教训

大家好,我是G探险者!

在实际开发中,我们经常会遇到 Java 对象需要序列化成 JSON 字符串的场景。然而,如果使用不当,很可能会出现 null 字段"神秘消失" 的情况。本文将通过一个真实案例,完整分析问题成因及解决方案。

一、问题背景

在项目中,我们有一个统一响应类 InvokeResult,构造方法如下:

java 复制代码
public InvokeResult(StatusCode statusCode, Object result) {
    this.code = statusCode.getCode();
    this.message = statusCode.getReasonPhrase();
    this.result = result;
}

按理说,result 是个 Object,谁传什么就按原样存。 但在一次调试中,我发现 result 内部很多字段值为 null,Debug 里都能看到这些字段,可一旦调用:

java 复制代码
result.getResult().toString();

这些 null 字段就凭空消失了。


二、现象描述

Debug 截图(简化版):

json 复制代码
{
    "monitor_level": "01",
    "input_user_id": null,
    "check_user_id": null,
    "mod_datetime": null,
    "post_code": null
}
  • 在调试器中 :这些 null 字段清清楚楚地存在。
  • 在 toString() 输出后:只剩下非 null 的字段,所有 null 都消失。

三、定位的"卡点"

这个问题根因并不复杂,但我定位过程却耗费了不少时间,原因在于:

  • InvokeResult 的构造方法参数类型是 Object,所以第一反应不会想到它是个 JSON 对象。
  • 实际调用过程中,result 传入的是 com.alibaba.fastjson.JSONObject。
  • 多态的"隐身攻击" :Object.toString() 在运行时,其实会执行 JSONObject 重写的 toString() 方法,而不是 java.lang.Object 原始实现。

这就导致了我一开始一直以为只是简单的字符串化操作,没想到背后走的是 Fastjson 的序列化逻辑。


四、真相揭秘

  1. Fastjson 的 JSONObject 重写了 toString()

    java 复制代码
    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }
  2. Fastjson 默认过滤 null

    java 复制代码
    JSON.toJSONString(obj); // 默认不输出 null
  3. 所以当你调用:

    java 复制代码
    result.getResult().toString();

    实际等价于:

    java 复制代码
    JSON.toJSONString(result.getResult());

    而不是普通的 Object.toString(),null 自然就被"优化"掉了。


五、解决方案

方案 1:显式保留 null

java 复制代码
return JSON.toJSONString(result.getResult(), SerializerFeature.WriteMapNullValue);

方案 2:避免直接用 JSONObject 做业务数据

java 复制代码
Map<String, Object> map = new HashMap<>();
map.put("input_user_id", null);
...
new InvokeResult(StatusCode.SUCCESS, map);

方案 3:全局配置 Fastjson 保留 null

java 复制代码
JSON.DEFAULT_GENERATE_FEATURE |= SerializerFeature.WriteMapNullValue.getMask();

六、经验教训

  1. 多态坑点 :方法签名是 Object,但实际类型可能是任何实现类,要随时留意运行时类型。

  2. toString 不一定安全 :特别是第三方库的对象,toString() 很可能被重写成带业务逻辑的方法。

  3. 调试时多用 getClass():

    java 复制代码
    System.out.println(result.getResult().getClass());

    一眼就能看出是不是自己想象的类型。


七、总结

这个问题的根因,其实一句话就能说清: JSONObject.toString() 默认会丢掉 null 字段,因为它走的是 Fastjson 的 JSON 序列化逻辑。

但它的难点在于:

  • 表面上你在用 Object.toString(),实则调用了子类的重写方法。
  • 参数是 Object,传进来的到底是什么类型,如果不特意去查,很容易忽略。
  • 调试时看到的对象和最终输出的字符串差异,容易让人把注意力放在 JSON 配置上,而不是类型本身。
相关推荐
一条Go4 小时前
两套权限目录,三重闸门:Alioth 的多租户 RBAC 为什么这么拧
后端·程序员·github
Tokenfree5 小时前
把多模态 OCR 接进业务系统:四千张运单,三个坑
后端
用户859313061355 小时前
我给 AI Agent 写了个下载器:293KB 的网页,不再吃掉 7 万 token
后端
phil65 小时前
沙箱核心功能系列 06:编排调度与资源管理
后端
小兔子5 小时前
if (system(cmd) != 0) 判不出命令失败:它返回的是等待状态,不是退出码
后端
艾迪的技术之路5 小时前
16k 条慢查询,128 张分表,我这么解决
后端·github
dpharness5 小时前
我踩过的四个记忆坑,从预算到换机
前端·后端
加宝的起点雷达5 小时前
三个定时任务被删那天,系统一声不吭
人工智能·后端
泡海椒5 小时前
jquick-pdf 热力图(HEATMAP)实战:24 格营收矩阵嵌入评级报告的模板逐行拆解
后端
对象存储与RustFS5 小时前
Velero 接 RustFS 实操:K8s 集群备份落到自建对象存储
后端·docker·云原生