JSON 结构化对比

按结构比而不是按行比。键顺序换了、缩进改了都不会误报,只有真正变了的键和值才会出现,并且带上能直接定位的路径。

对比选项

对象键顺序在 JSON 里本来就不携带含义,所以无论开关怎么设都不会误报。三个开关只影响值的比较方式,用来把机械差异滤掉、留下业务上真正关心的改动。

为什么不用文本行对比

文本对比看的是行。把缩进从 2 个空格改成 4 个,或者用工具重新格式化了整个文件,文本对比会满屏标红,但 JSON 的语义一点没变。反过来,如果两个人改了同一个键的值,文本对比只告诉你「这一行变了」,不会告诉你路径是什么。JSON 是有结构的,按树比能得到精确得多的答案。

键顺序同理。{"a":1,"b":2} 和 {"b":2,"a":1} 在 RFC 8259 里是完全相同的 JSON 值,序列化时键的顺序是实现细节。所以这个工具默认按集合比较键,顺序不同不报差异——这也正是它比文本对比更适合核对配置文件和接口响应的原因。

对比结果怎么读

路径用的是 JSONPath 的常用子集:根是 $,对象成员用点号连接,非标识符的键写成方括号加引号($["user-name"]),数组用下标($.list[0])。复制出来可以直接粘进 jq、Grafana 或者各种 JSONPath 工具继续用,不用再手工数层级。

四类差异分别是:新增指右边的文档多了一个键或数组元素,删除指左边有而右边没有,修改指同一位置的标量值变了,类型变化指这个位置整体换了种类(比如原来是对象,现在是数组)。会把类型变化单独拎出来,是因为这种情况在文本对比里往往只表现为一小段改动,但它对程序来说是最容易出问题的改动。

三个开关分别在什么时候用

忽略字符串大小写适合比较状态字段。RUNNING 和 running 在多数系统里是同一个状态,只是序列化风格不同,这类噪声滤掉能让你看清真正的改动。数字与字符串视为相同对接口回归测试特别有用,因为同一个字段有时返回 1 有时返回 "1",做功能验证时往往可以放过;但如果你是在校对接口契约,建议关掉,因为类型不一致本身就该被暴露。

忽略数组顺序的用法更微妙。数组在 JSON 里默认是有序的,[1,2] 和 [2,1] 是两份不同的数据,所以默认按位置逐个比。但核对用户清单、权限列表这类数据时,顺序常常只是查询没带排序造成的噪声。开启后工具会先找出内容一致的项,再对剩余项按字段重合度配对,这样「某条记录的某个字段改了」会报成一条精确的修改,而不是含糊的删除加新增。

5 个容易踩的点

JSON 对象的键顺序没有含义。RFC 8259 明确说对象是无序的键值对集合,所以 {"a":1,"b":2} 和 {"b":2,"a":1} 在语义上完全相同。这个工具默认就按集合来比,键顺序不同不会报差异;反过来,如果两份文件只是格式化缩进不同,这里也不会出现任何红绿,这正是它比文本行对比更适合看配置和接口的地方。

数组顺序默认是有含义的。数组和对象不同,[1,2] 和 [2,1] 是两份不同的数据,所以默认按位置逐个比。但对比「用户清单」「权限列表」这类数据时,顺序往往只是查询没排序造成的噪声,这时打开「忽略数组顺序」会先找出内容一致的项,再对剩余项按字段重合度配对,从而把结果收敛成「第 3 个用户的角色从 viewer 变成了 admin」这种能直接读懂的结论。

解析阶段会丢掉重复键。JSON.parse 遇到重复键时保留最后一个,所以 {"a":1,"a":2} 解析后只剩 a 为 2,这一点无法在解析后再补救。如果怀疑输入有重复键,用文本对比或者专门的 JSON 检查工具先看一眼更稳妥。同理,超大的整数(超过 2 的 53 次方)在 JavaScript 里会丢精度,订单号这类长数字建议在接口里就以字符串传。

数字和字符串混用是接口的常见毛病。同一个字段有时返回 1 有时返回 "1",常出现在后端语言序列化行为不一致或者缓存里存的类型不同。打开「忽略数字与字符串的区别」后,这类差异会被视为相同,同时 true 和 "true" 也会归一。做回归测试对比两次响应时这个开关很有用,但正式校验接口契约时建议关掉,因为类型不一致本身就该被暴露出来。

路径的写法。根用 $ 表示,对象成员用点号连接($.user.name),非标识符的键用方括号加引号($["user-name"]),数组用下标($.list[0])。这套写法就是 JSONPath 的常用子集,复制出来可以直接粘进 jq、JSONPath 测试工具或者 Grafana 之类的地方继续用。