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 配置上,而不是类型本身。
相关推荐
金色天际线-1 小时前
Nginx 优化与防盗链配置指南
java·后端·spring
weixin_456904278 小时前
Spring Boot 用户管理系统
java·spring boot·后端
cyforkk9 小时前
Spring 异常处理器:从混乱到有序,优雅处理所有异常
java·后端·spring·mvc
程序员爱钓鱼9 小时前
Go语言实战案例-开发一个Markdown转HTML工具
前端·后端·go
桦说编程10 小时前
爆赞!完全认同!《软件设计的哲学》这本书深得我心
后端
thinktik10 小时前
还在手把手教AI写代码么? 让你的AWS Kiro AI IDE直接读飞书需求文档给你打工吧!
后端·serverless·aws
老青蛙12 小时前
权限系统设计-用户设计
后端
echoyu.12 小时前
消息队列-初识kafka
java·分布式·后端·spring cloud·中间件·架构·kafka
yuluo_YX13 小时前
Go Style 代码风格规范
开发语言·后端·golang