JSON 对比

比较两段 JSON,列出新增、删除与修改的键路径

JSON 对比:为什么文本 diff 看 JSON 会满屏噪声

文本 diff 与结构化 diff 的差别

把两个 JSON 用文本 diff 工具比较,会出现两类问题:

  • 格式差异被当成内容差异。A 是缩进 2 空格、B 是压缩成一行,文本 diff 会认为整段都变了。实际上数据完全一致。
  • 键顺序差异被当成内容差异。JSON 对象是无序的(规范明确如此),{"a":1,"b":2} 与 {"b":2,"a":1} 语义相同,但文本 diff 会标出两处删除两处新增。

结构化 diff 的做法是先解析成对象再比较:忽略空白与键顺序,只在真实的路径上报告差异。输出的形式是"哪个路径、从什么变成了什么",这比行号定位有用得多——尤其在深度嵌套的配置或接口响应里,行号几乎不提供信息。

四个必须明确的判定规则

比较 JSON 时,有几个地方"看起来一样"和"确实相同"不是一回事,工具必须选定规则并说明:

情况是否算差异说明
1 vs 1.0通常不算数值相等;但字符串形式不同
1(数字)vs "1"(字符串)算类型不同,JSON 里它们是两种值
键不存在 vs 值为 null算"没有这个字段"和"字段是空"语义不同,接口里尤其如此
true vs "true"算布尔与字符串,不能混用

第三行是最常见的争议点。在 JSON Merge Patch(RFC 7396)这类规范里,null 被用来表示"删除该键",正是因为这两个状态必须区分开。

数组比较的特殊困难

对象有键可以对齐,数组没有。所以"[1,2,3] 变成 [3,2,1]"这件事有两种解释:

  1. 元素被重新排序——如果数组本身是有序集合(比如按时间排的用户列表),这是重排。
  2. 三个元素各自被替换——如果把数组当无序集合看,那全部内容没变。

工具无法知道你的意图,所以必须选一种算法。常见做法是基于最长公共子序列(LCS),它能识别出"某几个元素保留、某几个被插入或删除",对中间插入新元素的情况表现最好——而这恰恰是实际开发中最常见的变更(往列表里加一项)。纯按下标逐位比较的实现在插入场景下会把后面所有元素都标成"变了",噪声极大。

还有一层:如果数组元素是对象,且有你已知的唯一键(比如 id),按 id 对齐比按位置对齐准确得多。这只是通用的结构算法做不到的,需要针对数据结构定制。

它做不到的事

  • 不理解语义重命名。把 userName 改名成 username,工具只能报"删除了一个键、新增了一个键",它不知道这是一次改名。
  • 不校验结构合法性。两边都是合法 JSON 才比得下去;结构不符合你的接口约定不会被发现,这需要 JSON Schema。
  • 大文件性能。结构化 diff 的复杂度在嵌套深、数组大时会明显上升。几 MB 的 JSON 建议先用 jq 之类的工具裁剪到关心的子树再比。

实用工作流

  1. 先确认两边都是合法 JSON(本工具会在解析失败时直接报错,而不是给出错误的对比结果)。
  2. 看差异的路径,而不是位置。路径能直接告诉你"是配置里的某一项变了"。
  3. 数组出现大量差异时,先考虑是不是元素顺序变了,或者是不是该按唯一键对齐。
  4. 如果结果是给 diff 评审用的,先用键排序工具把两边都排序再比,可以消掉键顺序带来的噪声。

常见问题

数组顺序变了怎么算?

按相同下标逐项比较。如果元素只是位置移动,对应下标的旧值和新值不同,会被标记为"修改"。

对比会忽略字段顺序吗?

会。对象的键顺序不同但内容相同,视为无差异;只有值的增删改才会被列出。