一提到JSON序列化,很多人第一反应是:不就是JSON.stringify或者json.dumps吗?有什么好聊的。但我在线上环境排查过太多由序列化引发的事故:大整数悄悄变了值、接口返回的日期格式前后端各执一词、某个安全公告突然说框架自带的反序列化组件能被人远程利用。这篇文章就围绕JSON序列化问题,把我这些年踩过的类型精度、跨语言差异、消息队列集成、以及反序列化安全相关的坑,一次性梳理清楚。内容主要围绕JSON序列化与反序列化这条主线展开,适合正在处理接口联调、消息队列、数据配置文件解析,或者被反序列化漏洞通告折腾得焦头烂额的开发同学。
1. 序列化的本质:为什么数据要“打包”再“拆包”
1.1 内存里的对象和磁盘/网络上的数据,完全是两种形态
程序运行中的对象是什么?是一块内存区域,里面除了业务字段,还有对象头、引用指针、方法表地址。同样是“用户”这个概念,Java里的User对象、Python里的dict、PHP里的数组,在各自进程里的内存布局天差地别。你想把它写到文件里、塞进Redis、丢给RabbitMQ,就必须把这块内存“翻译”成一种可传输、可存储的字节序列,这个过程叫序列化;对方收到后再按自己语言的规则“重建”对象,就是反序列化。
很多初学者不理解为什么序列化会引发问题。我打个比方:内存对象就像你脑子里的一段信息,序列化是把它写到一张纸上,反序列化是对方看完这张纸再把信息记回自己脑子里。写的时候用中文还是英文、保留了哪些细节、丢失了哪些字段,都会影响对方最终是否理解正确。JSON只是这张“纸”的排版规范之一,它规定了字段怎么写、数组怎么排、数字和字符串怎么区分,但它并没有规定“信息传到之后必须100%还原”,这中间的偏差就是无数线上事故的根源。
1.2 JSON能成为默认格式,靠的是自描述和跨语言
JSON全称JavaScript Object Notation,最初只是JS对象字面量语法的子集,后来独立成标准。它一共只有6种值:字符串、数字、布尔、null、数组、对象。这意味着任何一门语言,只要实现“字符串解析”和“对象映射”就能处理JSON,这是它能跨语言流行的基础。
自描述性体现得很直观:{"name":"张三","age":30}这段文本,哪怕没有任何额外文档,你也能看出有两个字段、分别是什么类型。相比XML,JSON更轻量;相比Protocol Buffers,JSON不需要预先定义proto文件,也不需要编译生成代码,改起来自由。所以项目里哪怕内部存储用了二进制格式,接口层、配置文件、消息体大部分仍然用JSON。这一点在热搜词里也体现得很明显:json格式、json转换、json学习,几乎每个开发者都绕不开它。
1.3 序列化和反序列化,其实是格式协商的过程
序列化最容易被忽略的一点:它是一个“双方协商”的过程。同一个JSON字符串,在Java里反序列化成HashMap,在Python里是dict,在PHP里可能是stdClass对象或关联数组,这取决于解析库的默认行为。字段命名是驼峰还是下划线、日期格式是什么、null字段是否输出、数字解析成int还是long,这些都直接影响通信双方对同一份数据的理解。
所以我先给一个总原则:写序列化代码时,默认使用显式配置,不要依赖库的默认行为。这个原则在后面每个章节都会反复出现。很多人出问题,不是不会用某个库,而是相信了“默认行为是合理的”这个假设。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 对象到JSON这条路上,我遇到的五类典型事故
2.1 大整数精度丢失:一个ID从Java到前端变了两位数
有一次联调,后端给我返回一个用户ID字段:"id": 738559821948731392,前端拿到后打印出来变成了738559821948731400。数值变了,排查半天发现是JavaScript的Number类型精度问题。
JavaScript的Number是双精度浮点数,能精确表示的整数范围只有正负2的53次方减1,也就是9007199254740991。Java的Long是64位有符号整数,最大能到9223372036854775807,很多分布式ID用雪花算法生成的长整型,轻轻松松超出JS的精确范围。结果就是:后端序列化没做任何处理,前端JSON.parse时精度就丢了,而且通常表现为末尾几位变成0,肉眼很难察觉。
处理方式我一般用两种:
- 在Jackson中,给Long类型的字段配置
ToStringSerializer,例如@JsonSerialize(using = ToStringSerializer.class),或者全局统一配置,让所有Long字段序列化成字符串。 - 数据库主键、分布式ID这类不需要参与计算的字段,干脆在定义阶段就用字符串类型,从源头
