Java Jackson 多态 JSON 怎样保留类型信息
来源:17golang原创
时间:2026-10-14 13:06:38 126浏览 收藏
Java 对象声明为父类时,Jackson 需要额外的类型信息,才能在读取 JSON 时确定具体子类。面向接口或持久化消息,通常可以在父类型上配置 @JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "kind"),再通过 @JsonSubTypes 明确映射。保存的是稳定的业务类型名称,不必把 Java 包名当成协议的一部分。
Jackson 官方项目地址:https://github.com/FasterXML/jackson-databind
从普通通知对象到带类型的协议
考虑一个通知接口:同一字段 notice 有时装文字通知,有时装带链接的通知。服务内部有 TextNotice 和 LinkNotice,对外统一声明为 Notice。仅凭共有的标题,读取端无法知道应创建哪个具体对象。
加入 kind 后,变化的是接口契约,不是 Jackson 突然获得了一个新发布的功能。设计目标是让 text 与 link 代表通知语义:以后重命名内部类,外部名称仍可保持不变。代价是双方必须约定类型词表,并处理新增类型时的兼容问题。
先确定类型名称,再映射实现类
结构由六个节点组成:Envelope 是消息外壳,Notice 是父类型,kind 是类型字段,类型映射 关联 TextNotice 和 LinkNotice。这里的映射仅接受程序登记的逻辑名称,不根据输入中的任意类名选择实现。

不要采用“类名改了,JSON 也跟着改”的默认命名约定。显式填写 name = "text" 比依赖类的简单名称更容易保持契约稳定。类型名称也不是权限凭证:识别出链接通知,并不代表用户有权执行某种动作。
一个完整的 Jackson 2.x 往返例子
下面是 NoticeJsonDemo.java 的教学代码,使用 Jackson 2.x 的 com.fasterxml.jackson.databind 包。项目应通过自身依赖管理对齐 databind、core 和 annotations,不能随意混用版本。Jackson 3.x 的 databind 包为 tools.jackson.databind,这份示例不能直接当成 3.x 配置代码照搬。
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
public class NoticeJsonDemo {
// 稳定的逻辑类型名称属于 JSON 契约,不使用 Java 类全名。
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY, property = "kind")
@JsonSubTypes({
@JsonSubTypes.Type(value = TextNotice.class, name = "text"),
@JsonSubTypes.Type(value = LinkNotice.class, name = "link")
})
public static abstract class Notice {
public String subject;
}
// 教学用公开字段;隐式无参构造允许 Jackson 创建实例。
public static class TextNotice extends Notice {
public String text;
}
public static class LinkNotice extends Notice {
public String target;
}
public static class Envelope {
public String messageId;
public Notice notice;
}
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
// 明确保留未知或缺失类型的错误,不静默替换为 null。
mapper.enable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE);
TextNotice text = new TextNotice();
text.subject = "演示提醒";
text.text = "任务材料已准备";
Envelope source = new Envelope();
source.messageId = "demo-42";
source.notice = text;
String json = mapper.writerFor(Envelope.class)
.writeValueAsString(source);
Envelope restored;
try {
restored = mapper.readValue(json, Envelope.class);
} catch (InvalidTypeIdException ex) {
// 真实接口应映射到受控错误响应,不将内部异常暴露给客户端。
throw new IllegalArgumentException("通知类型缺失或不受支持", ex);
}
// 用显式断言检查对象语义,不依赖 JSON 属性的输出顺序。
if (!(restored.notice instanceof TextNotice)) {
throw new IllegalStateException("通知子类型未恢复");
}
TextNotice result = (TextNotice) restored.notice;
if (!"任务材料已准备".equals(result.text)
|| !"demo-42".equals(restored.messageId)) {
throw new IllegalStateException("消息数据不一致");
}
System.out.println(json); // 仅输出本例的虚构数据。
}
}
按此契约,JSON 的结构应类似下面的示意值,属性顺序不作保证。这不是本轮运行输出:
{
"messageId": "demo-42",
"notice": {
"kind": "text",
"subject": "演示提醒",
"text": "任务材料已准备"
}
}
kind 供类型解析使用,text 是通知的业务内容,二者用途不同。上述模型没有额外声明一个可变的 kind 字段,因此不需要把类型元数据传给普通字段绑定。
保留类型不等于增加一个同名字段
容易出现的误改是给父类添加 public String kind,同时仍要求 Jackson 自动写同名类型元数据。这样会把协议分派与业务字段的赋值责任混在一起。需要在业务模型中访问类型字段时,应先明确它由谁产生、如何防止它与实际对象类型矛盾,再选择适合的包含方式。
官方注解说明中,visible = true 控制读取时是否把类型标识继续交给普通属性绑定;它不是“打开多态”的开关。As.EXISTING_PROPERTY 则依赖常规属性负责写出类型名称,不能和自动生成元数据的方式混为一谈。本例保持默认可见性,不添加重复属性。
集合也应保留元素的声明类型。把整个列表读取为裸 List.class,不能代替 List 的类型信息;可以用带泛型的消息字段,或用 TypeReference 描述根集合。根列表写出时也应使用明确的元素类型,不只依赖运行时的泛型擦除结果。>
旧 JSON 没有 kind,不能靠新注解补出历史信息
迁移中的六个节点是旧消息、适配层、新消息、类型注册、已知类型与未知类型。旧消息缺少标识时,适配层只能依据已有的可信来源约束作转换;新消息携带标识,类型注册决定哪些名称可解析。未知类型必须有明确的拒绝或隔离策略。

如果旧接口明确只允许文字通知,可以在受控的旧版本入口进行专门转换。如果历史数据本来就混合多种通知,且字段不足以唯一判断,就应补齐业务来源或修复数据,不能随意给所有记录添加 "kind": "text"。
不要把关闭 FAIL_ON_INVALID_SUBTYPE 当成协议兼容方案。在本例的抽象父类型且没有默认实现的设置下,类型缺失或无法映射应作为错误处理;关闭相应失败策略可能让值变成 null,问题只是被推迟到业务层。defaultImpl 也需要具体迁移语义,并不会自动解决字段结构不匹配。
| 协议变化 | 兼容处理 |
|---|---|
| 内部类重命名 | 保留既有逻辑名称,更新内部映射 |
| 新增一个 kind | 先更新读取端能力,再按发布约定启用写出 |
| 旧数据没有 kind | 依据可信旧协议转换,无法判断则隔离处理 |
| 已知类型新增字段 | 另行约定字段必选性和未知字段处理 |
| 不认识的 kind | 返回受控错误或保留为不可执行业务的隔离记录 |
读者可以怎样检查迁移结果
在项目测试中,分别准备 text 和 link 的样本,检查读取后的具体子类与业务字段;再准备缺失 kind 和未知 kind 的输入,检查它们是否进入预期错误分支。还应检查旧版读取端收到新增类型时的行为。不要仅凭“序列化没有抛异常”就宣称双向兼容。
链接通知中的 target 在本例只是字符串,没有网络访问或跳转实现。真实应用还要根据其用途进行地址策略、权限、大小限制及业务字段校验。显式类型名称和有限子类型映射缩小了类型选择范围,但不能替代全部安全控制,也不能为不受信任输入开启任意类名解析。
官方类型处理资料:https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization
官方注解定义:https://github.com/FasterXML/jackson-annotations/blob/2.x/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java
两个相关问题
@JsonSubTypes 单独能开启多态吗?它描述子类型关联,但还需要类型信息策略。不要把类型词表当成完整的读写配置。
多态读取成功,新增类型就对所有客户端兼容吗?不是。未知类型的处理由各读取端决定,新增名称可能让旧端拒绝消息。类型字段稳定、词表发布顺序和错误语义,都属于接口契约的一部分。
保留多态类型信息的可靠做法,是让 JSON 表达业务类型,让代码维护有限映射,再为旧数据和未知类型制定明确规则。类名可以调整,协议名称不应随意漂移。
-
文章 · java教程 | 1个月前 | Java · 异常处理 · 资源管理 · java try-with-resources AutoCloseable close suppressed exception501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
311 收藏
-
306 收藏
-
110 收藏
-
422 收藏
-
281 收藏
-
279 收藏
-
文章 · java教程 | 2小时前 | Java · 异常处理 · try-with-resources IOException Java InputStream.transferTo Java 字节流复制 OutputStream455 收藏
-
499 收藏
-
180 收藏
-
444 收藏
-
文章 · java教程 | 2小时前 | 异步编程 · Java教程 · CompletableFuture · 进程管理 · java completablefuture process ProcessHandle onExit 异步进程233 收藏
-
370 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习