当前位置:首页 > 文章列表 > 文章 > java教程 > Java Jackson 多态 JSON 怎样保留类型信息

Java Jackson 多态 JSON 怎样保留类型信息

来源:17golang原创 2026-10-14 13:06:38 0浏览 收藏

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。这里的映射仅接受程序登记的逻辑名称,不根据输入中的任意类名选择实现。

通知消息类型字段与显式子类型映射结构图
消息边界中的 kind 是协议标识;类型映射把 text、link 关联到已登记实现。连线表达静态包含与映射关系,不是执行步骤。

不要采用“类名改了,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 表达业务类型,让代码维护有限映射,再为旧数据和未知类型制定明确规则。类名可以调整,协议名称不应随意漂移。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Java Maven BOM 怎样统一依赖版本Java Maven BOM 怎样统一依赖版本
上一篇
Java Maven BOM 怎样统一依赖版本
Java Gradle configuration cache 不生效怎么办
下一篇
Java Gradle configuration cache 不生效怎么办
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    469次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    530次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    549次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    499次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    317次使用