Java 多态 JSON 映射的类型边界
我第一次把“订单事件”和“用户事件”放进同一个 JSON 字段时,真正难处理的不是序列化,而是边界:Java 代码知道有哪些子类型,JSON 却可能带来未知的 kind。比较稳的做法是把两层约束同时写出来:用 sealed interface 固定 Java 允许的事件集合,用 Jackson 的逻辑类型名完成 JSON 映射,并让未知类型在反序列化入口明确失败。
这篇只讨论判别字段、反序列化和未知类型,不把 JSON 中的类名当成协议。Java 17 已支持 sealed 类型,下面的语法在 JDK 17 及更高版本都可作为迁移基线。
Java 官方资料:https://docs.oracle.com/en/java/javase/26/
Jackson 多态类型资料:https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization
先把升级范围和类型边界说清楚
这次调整可以看成一次小型协议迁移:旧模型只声明一个抽象父类型,新的模型额外声明允许的子类型和 JSON 判别字段。sealed 只限制 Java 类型层次,不会自动生成 JSON 字段,也不会替 Jackson 判断外部输入是否可信。
| 位置 | 迁移前的风险 | 迁移后的约束 |
|---|---|---|
| Java 类型 | 任意实现类都可能进入事件字段 | sealed interface + permits 固定直接子类型 |
| JSON 判别 | 依赖类名或隐式推断,协议易泄漏实现细节 | 使用稳定的 kind 逻辑名称 |
| 未知输入 | 缺少或新增类型时行为不清楚 | 在反序列化边界保留无效类型检查并记录失败 |
这里的“类型边界”有两条:一条是编译期的 permits 集合,另一条是运行时 JSON 的 kind 值。只有两条边界都明确,迁移后的代码才不会把协议问题推迟到业务分支。
把 sealed 子类型映射成稳定的 kind
我更倾向于把 JSON 中的类型名设计成业务协议的一部分,例如 user-created 和 order-paid,而不是使用完整 Java 类名。Jackson 的 @JsonTypeInfo 负责声明判别字段,@JsonSubTypes 把逻辑名称绑定到允许的 Java 子类型。
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
// 只允许协议明确列出的两种领域事件进入这个抽象类型。
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "kind"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = UserCreated.class, name = "user-created"),
@JsonSubTypes.Type(value = OrderPaid.class, name = "order-paid")
})
sealed interface DomainEvent permits UserCreated, OrderPaid {
}
// record 让事件字段保持不可变,避免反序列化后再被随意改写。
record UserCreated(String userId) implements DomainEvent {
}
record OrderPaid(String orderId, long amount) implements DomainEvent {
}

这段声明解决了三个容易混淆的问题:DomainEvent 表达允许集合,kind 表达线上协议,Jackson 注解表达两者的映射。它没有声称 JSON 本身可信,也没有把任意类名交给反射机制。
把反序列化失败点放在输入边界
迁移时最容易漏掉的是“未知类型怎么办”。下面的配置把 JSON 读成抽象类型,并显式保留无效子类型检查。示例里的 JSON 是协议样例,不是本机运行输出。
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
// 在输入边界拒绝未知 kind,避免未知事件悄悄变成错误的默认对象。
var mapper = JsonMapper.builder()
.enable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE)
.build();
// kind=user-created 会映射到 UserCreated,kind=order-paid 会映射到 OrderPaid。
String json = "{\"kind\":\"order-paid\",\"orderId\":\"O-17\",\"amount\":199}";
DomainEvent event = mapper.readValue(json, DomainEvent.class);
// 业务分支只处理编译器知道的事件集合,不接收任意实现类。
String description = switch (event) {
case UserCreated created -> "用户 " + created.userId();
case OrderPaid paid -> "订单 " + paid.orderId();
};
当 kind 是 invoice-issued 这类未知值时,Jackson 应在读取阶段报告无效类型标识;当判别字段缺失时,也应把它当作协议不完整处理。不要为了“兼容”随便设置一个默认子类,否则新增消息可能被错误地解释成旧事件。
Jackson 官方文档把多态处理定义为给反序列化器补充足够的类型信息。这里选择 Id.NAME,就是把稳定的逻辑名称放进 JSON,而不是把包名、实现类名和内部重构细节暴露给调用方。

从旧模型迁移时要回归哪些情况
我会把回归样例分成四组,而不是只测一条正常 JSON。这样可以把“协议新增”和“代码新增”区分开,也能提前发现旧消息在判别字段上的兼容问题。
- 已有类型:保留一条
user-created和一条order-paid,确认字段名称、数值类型和业务分支都没有变化。 - 缺少 kind:验证输入被拒绝,并把异常转换为接口层的明确错误,不让它进入领域事件处理器。
- 未知 kind:使用一个尚未登记的逻辑名称,确认不会落到默认对象,也不会因为类名猜测而放宽范围。
- 新增子类型:先更新
permits、Jackson 类型映射和回归样例,再发布能产生新kind的生产方。
还要注意 Java 自身的兼容边界:Oracle 的语言规范说明,改变 sealed 接口的允许子类型集合可能影响已有二进制;如果旧实现不在新的 permits 列表中,升级不能只改 JSON 注解,还要安排实现方同步升级。
| 检查项 | 通过标准 | 不通过时的处理 |
|---|---|---|
| 类型集合 | Java 子类型和协议文档一致 | 先补齐 permits 与映射,再开放新 kind |
| 判别字段 | kind 稳定且不含 Java 包名 | 保留旧值,新增值走版本化迁移 |
| 异常边界 | 缺失或未知 kind 进入统一错误处理 | 禁止默认对象兜底,补充回归样例 |
这套边界适合什么项目
如果事件集合相对稳定、消息由多个服务交换,而且你希望编译器提醒新增分支,sealed + 逻辑 kind 是一套清楚的组合。它的代价也很明确:新增子类型要同时改 Java 类型集合、JSON 映射和回归测试;如果你的插件系统必须允许外部团队自由扩展,就不应把 sealed 当作通用扩展机制。
我的迁移结论是:先确定协议里的判别字段,再把它绑定到受控的 Java 子类型集合,最后决定未知输入是拒绝、隔离还是进入专门的兼容事件。不要让 Jackson 的默认推断替你做这个架构决定。
相关问题
sealed interface 能自动完成 JSON 多态映射吗?
不能。sealed 约束 Java 的继承集合,JSON 的判别字段仍需要 Jackson 注解或显式模块配置。
为什么不直接把 Java 类名写进 JSON?
类名会把包结构和实现细节变成外部协议,重构和跨服务迁移的成本更高。稳定的逻辑名称更适合长期消息格式。
未知 kind 是否应该映射到一个 UnknownEvent?
只有在业务明确需要保留原始消息时才这样设计,并单独定义隔离模型。不要用默认子类掩盖协议升级或输入错误。
Java Stream 分组后保持排序的收集方案
- 上一篇
- Java Stream 分组后保持排序的收集方案
- 下一篇
- Java 模块路径和类路径冲突的定位清单
-
- 文章 · java教程 | 1小时前 | Java · Java教程 · java classpath module-info.java module-path 模块可见性
- Java 模块路径和类路径冲突的定位清单
- 210浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java Stream 分组后保持排序的收集方案
- 104浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java HttpClient ofFileDownload 怎样保存响应文件名
- 365浏览 收藏
-
- 文章 · java教程 | 1小时前 | 超时 · Java ·
- Java HttpClient 分层超时的配置方法
- 344浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java MethodHandle invokeExact 类型不匹配为什么会失败
- 494浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java 虚拟线程遇到阻塞调用时的线程占用分析
- 401浏览 收藏
-
- 文章 · java教程 | 2天前 | 线程池 · Java教程 · java 并发编程 ForkJoinPool ForkJoinTask managedBlock ManagedBlocker
- Java ForkJoinPool managedBlock 怎样避免阻塞任务拖住并行计算
- 489浏览 收藏
-
- 文章 · java教程 | 2天前 |
- Java DatagramChannel 怎样设置非阻塞接收
- 337浏览 收藏
-
- 文章 · java教程 | 2天前 |
- Java WebSocket.Listener 怎样控制下一条消息的接收节奏
- 370浏览 收藏
-
- 文章 · java教程 | 2天前 |
- Java Flow Publisher 怎样实现背压协议
- 339浏览 收藏
-
- 文章 · java教程 | 2天前 |
- Java CompletableFuture resultNow 怎样读取已完成结果
- 302浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 505次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 563次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 589次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 540次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 352次使用
-
- 接口返回 200 但前端仍报错怎么办:从响应格式到跨域一步步排查
- 2026-06-14 332浏览
-
- golang生成JSON以及解析JSON
- 2023-01-17 329浏览
-
- Go如何实现json字符串与各类struct相互转换
- 2023-01-07 377浏览
-
- Go Java 算法之字符串解码示例详解
- 2023-01-07 479浏览
-
- Go Java算法之单词搜索示例详解
- 2022-12-30 337浏览
