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 小时前
基于大数据的美食视频播放数据可视化系统 Python+Django+Vue.js
大数据·前端·javascript·vue.js·spring boot·后端·python
Re2752 小时前
我用4碗面讲清HTTP的四大请求方法:GET/POST/PUT/DELETE
后端
有梦想的攻城狮2 小时前
spring中的ApplicationRunner接口详解
java·后端·spring·runner·application
程序视点2 小时前
设计模式之原型模式!附Java代码示例!
java·后端·设计模式
用户21411832636023 小时前
AI 驱动开发:20 分钟搞定智能发票申请单系统
后端
振鹏Dong3 小时前
微服务架构及常见微服务技术栈
java·后端
程序员爱钓鱼4 小时前
Go语言实战案例:简易JSON数据返回
后端·go·trae
程序员爱钓鱼4 小时前
Go语言实战案例:用net/http构建一个RESTful API
后端·go·trae
bobz9654 小时前
firewalld 添加 nat 转发
后端