接口回归、配置审核、数据库快照对比——每次用 diff 命令比 JSON,十有八九被”假差异”淹没。原因很简单:JSON 有语义,文本 diff 没有。
两种 diff 的区别
文本 diff:按字符/行比,空格、换行、顺序任何一点不同都是差异。
语义 diff:先把两边解析成 JSON 树,再按规范比较——只看值和结构,忽略空白和书写顺序。
拿一组常见例子看:
A: {"name":"Alice","age":30}
B: {
"age": 30,
"name": "Alice"
}
- 文本 diff:从第 1 行开始全红全绿
- 语义 diff:完全相等
A: {"tags":["a","b"]}
B: {"tags":["b","a"]}
- 文本 diff:第 10 列字符不同
- 语义 diff:数组顺序差异(如按集合比则相等)
哪些”差异”其实不是差异
1. key 顺序
JSON 规范:object 的 key 是无序集合。序列化库输出顺序依赖实现(Python 3.7+ 保留插入序、Go 的 map 是随机序)。
2. 空白、换行、缩进
只影响可读性,不影响语义。{"a":1} 和 {\n "a": 1\n} 等价。
3. 字符串 Unicode 转义
"你好" 和 "你好" 是同一个字符串,JSON 规范允许两种写法。
4. 数值书写
1e2、100、100.0 在 JSON 数值语义下都是 100。但要注意精度:1.0 和 1 在强类型语言(Go、Java)里可能不同。
5. 尾随零
{"price":10.00} 解析后变 10,再序列化出来没有 .00——这不是”差异”,是 IEEE 754 本身就不存尾随零。
哪些差异文本 diff 会漏
1. 数组顺序变化
A: [1,2,3]
B: [1,2,3]
看起来一样——但如果 B 实际是 [1,2,3](含 BOM)或多一个空元素 [1,2,3,](非法),文本 diff 可能显示行相同而解析失败。语义 diff 会直接拒绝非法 JSON。
2. 类型变更
A: {"id": 123}
B: {"id": "123"}
字符数几乎一样,文本 diff 只报 " 多了两个。语义 diff 会明确告诉你 number → string——这种变更往往是后端升级把大整数转成字符串,前端若直接做算术会报错。
3. 键的新增/删除 vs 键的重命名
A: {"userId": 1}
B: {"user_id": 1}
文本 diff 报一行变了。语义 diff 报”删除 userId、新增 user_id”——这是破坏性变更,接口契约完全断了。
JSON diff 的标准输出路径
好用的 JSON diff 工具输出会按JSONPath 定位到每个差异:
$.user.name : "Alice" → "Bob" (string)
$.items[2].price : 9.9 → 10.0 (number)
$.tags : ["x","y"] → ["x"] (array shrank)
$.meta.createdAt : added "2026-04-22"
$.meta.requestId : removed
这种路径格式能:
- 直接用
jq '.user.name'验证 - 复制到代码里当 key 访问
- 作为忽略列表的表达式
业务里要不要”顺序敏感”
取决于数组的语义:
| 场景 | 数组性质 | 建议 |
|---|---|---|
| 评论列表 | 有序(按时间) | 顺序敏感 |
| 用户权限 | 集合 | 顺序不敏感 |
| 购物车 | 有序 | 顺序敏感 |
| 标签 | 集合 | 顺序不敏感 |
| 分页结果 | 有序 | 顺序敏感 |
| 枚举值列表 | 集合 | 顺序不敏感 |
工具不该替你决定,而该提供按路径配置的开关:$.permissions 用集合比,$.items 用顺序比。
接口契约对比的建议流程
- 抓两端响应,存成
a.json/b.json - 格式化两边(消除空白差异)
- 忽略动态字段:
timestamp、requestId、nonce - 语义 diff,输出 JSONPath 差异列表
- 分类:类型变化、字段新增/删除、值变化
- 打回:类型变化和字段删除是破坏性变更,必须版本号升级或兼容处理
为什么不建议用 git diff 比 JSON
git diff 是行级文本 diff,遇到:
- 压缩后的长 JSON 一行——全红全绿,完全看不出哪里变了
- 格式化过的 JSON——和原始单行对比显示整文件重写
- key 顺序变了——产生”假阳性”差异
正确做法:git 保留 JSON 的稳定格式(工具格式化后提交),对比用专用 JSON diff 工具,不用 git diff。
粘进去直接对比
左右两侧贴 JSON,实时输出 JSONPath 级差异;支持忽略 key 顺序、按集合对比数组、屏蔽指定路径——不用来回跑 jq 和 diff。