我先把话说在前面:JSON的序列化和反序列化,看起来是个谁都会用的基础功能,但一旦涉及“多态”,水就深了。很多项目做到后期,遇到的最隐蔽、最难排查的那类问题——字段丢失、类型不对、ClassCastException、甚至反序列化漏洞,根源常常就藏在这两个字里。这篇文章我就围绕“JSON序列化与反序列化中的多态处理”这个主题,把原理、方案、坑点、安全边界一次性讲透。不管你是写Java、Python、C++,还是做架构设计,只要你的系统里有继承和多态,这篇文章都值得你花十分钟读完,并在下次写DTO时想起它。
1. 多态信息是怎么在JSON链路里丢掉的
1.1 一次线上消费端“字段凭空消失”的故障
先说个我实际经历过的故障。
当时我们有个订单事件总线,上游服务把一个 OrderCreatedEvent 对象序列化成JSON扔到消息队列。这个 OrderCreatedEvent 继承自一个 BaseOrderEvent,里面除了公共字段 orderId、timestamp 之外,还有一个子类特有的字段 discountAmount。下游消费服务拿到消息后,反序列化成 BaseOrderEvent 去处理,结果 discountAmount 这个字段直接就是 null。
最诡异的是,JSON文本里明明有 discountAmount 这个字段,序列化那一端没有丢,可消费端反序列化之后它就是拿不到。
这其实是一个非常典型的多态序列化问题。上游序列化时,对象的动态类型是 OrderCreatedEvent,Jackson会把它的字段都写进JSON(包括继承来的和子类自己新增的)。但下游反序列化时,声明的目标类型是 BaseOrderEvent,Jackson只能依据这个静态类型去创建对象。你想想,BaseOrderEvent 这个类根本没有 discountAmount 属性,Jackson拿到那个字段后,默认策略是直接忽略掉,结果就是:数据明明就在JSON里,但你读出来是空的。
这个问题用下面这段代码就能复现:
java复制public class BaseOrderEvent {
public String orderId;
public long timestamp;
}
public class OrderCreatedEvent extends BaseOrderEvent {
public BigDecimal discountAmount;
}
// 序列化
OrderCreatedEvent event = new OrderCreatedEvent();
event.orderId = "A001";
event.timestamp = System.currentTimeMillis();
event.discountAmount = new BigDecimal("19.90");
String json = objectMapper.writeValueAsString(event);
System.out.println(json);
// 输出:{"orderId":"A001","timestamp":1720000000000,"discountAmount":19.90}
// 反序列化
BaseOrderEvent parsed = objectMapper.readValue(json, BaseOrderEvent.class);
System.out.println(parsed.discountAmount);
// 输出:null,而且编译不过,因为BaseOrderEvent根本没有这个字段
这段代码的核心矛盾在于:JSON只能描述“对象长什么样”,描述不了“这个对象是什么类型”。除非你显式地把类型信息也写进JSON里,否则反序列化端只能靠猜。
1.2 JSON的数据模型本身没有“类型系统”
JSON格式本身只定义了六种值类型:string、number、object、array、boolean、null。它没有一个叫“类”或“类型标签”的概念。换句话说,JSON里的每一个对象天然不知道自己是某个类的实例。
做序列化的库(Jackson、Gson、fastjson)之所以能把一个Java对象变成JSON,靠的是反射去遍历对象的属性和结构。但反序列化就麻烦了:当你给它一段JSON和你的目标类型时,它只能按照你给的目标类型去实例化对象,挨个字段匹配赋值。
这就好比你把一堆零件(字段)装进了一个没有任何标签的箱子里,运输过程中箱子是完整的,但收货方不知道这箱零件原本该组装成“椅子”还是“桌子”,他只能按照自己手里图纸上的型号去组装。如果图纸上写的是“桌子”,那箱子里属于“椅子”的配件自然就被当成多余零件扔掉了。
问题来了:Java、C++、Python、PHP这类面向对象语言里,多态恰恰意味着一个变量在编译期是父类型,在运行期却可能是任意一个子类型。父类型的“图纸”根本不包含子类型自定义的字段。所以,多态和JSON天然是不兼容的,你需要用某种约定去弥补这个缺口。
1.3 先确认你的业务是不是真的需要多态
在给方案之前,我想先说一句可能会得罪人的话:不是所有“多个子类继承父类”的场景都适合直接做多态JSON序列化。
我在项目里见过不少把多态JSON用得很别扭的案例。比如一个接口返回的DTO里定义了一个父类字段,然后实际塞进去的是某个子类,但下游消费方根本不需要关心子类是什么,所有端都只按父类字段处理。这种情况下,你完全可以把子类拍平成一个普通DTO,或者用组合代替继承,没必要为了“设计模式”而引入多态序列化的复杂度。
真正需要多态JSON的场景,通常有这几个特征:
| 场景 | 特征 | 例子 |
|---|---|---|
| 事件驱动架构 | 不同事件有公共属性,又有差异化属性 | OrderCreatedEvent、OrderCancelledEvent |
| 规则引擎/工作流 | 同一规则接口有多种实现,参数各不相同 | 不同审批节点的参数 |
| 配置中心 | 按类型加载不同配置结构 | 各业务线自定义配置模板 |
| 插件化系统 | 插件入口有标准接口,内部结构各异 | 报表插件参数 |
| 消息/任务分发 | 一个队列承载多种任务对象 | 任务中心的任务类型 |
如果你的业务不属于上面这些模式中的任何一种,那我建议你还是别用多态序列化,老老实实拆分字段,省下来的可能是一整年的排查时间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Jackson多态反序列化的标准解法:JsonTypeInfo
2.1 三个注解的配合逻辑
Java里用Jackson处理多态序列化,核心就是 @JsonTypeInfo 这个注解。它的作用就是序列化时在JSON里写入一个“类型标识字段”,反序列化时根据这个标识去实例化对应的子类。
我自己最常用的组合是:
java复制@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type",
visible = true,
defaultI
