HandyTools Hub

JSON 差异对比

比较两个 JSON 对象并即时发现差异

用肉眼比较两个 JSON 文档,一旦超过几行就不可靠:键顺序被打乱、空白格式不同,都会把真正重要的改动藏起来。这个工具会解析两边、递归遍历,只报告真实差异,并为每一项给出路径。

比较的工作原理

两个输入都通过 JSON.parse 解析。每一边的顶层都必须是对象;根节点是数组或原始值会被拒绝。然后工具取每一层键的并集并逐一遍历。

  • 如果某个键在两边都存在且两个值都是普通对象,就递归进入该对象,而不是把它当作叶子。
  • 其余情况——原始值、数组、null,以及只出现在一边的键——都作为叶子用深度相等比较。
  • 缺失的键记为 undefined:只出现在左边的键是删除,只出现在右边的是新增,两边都存在但值不相等的是修改。
  • 路径用点号连接,因此嵌套键会变成 address.city。
  • 最后,所有条目按路径字母排序,并给出四个计数。

由于数组从不被递归进入,工具把整个数组当作一个值。两个数组只有在长度相同且每个元素都深度相等时才相等。

使用方法

  1. 粘贴原始 JSON 到左侧编辑器。页面预填了一个小示例,方便你立刻看到行为。
  2. 粘贴修改后的 JSON 到右侧编辑器。
  3. 点击对比。 不按按钮不会运行任何计算。
  4. 查看结果。 绿色表示新增,红色表示删除,黄色表示修改,灰色表示未修改。修改项会先显示带删除线的旧值,再显示新值。

任意一边 JSON 无效都会产生指明该侧的解析错误,因此多余的逗号或缺失的引号很容易定位。

实操示例:一个 API 响应

以页面初始的两个示例对象为例。

左侧:

{
  "name": "Alice",
  "age": 30,
  "email": "alice@example.com",
  "address": { "city": "New York", "zip": "10001" }
}

右侧:

{
  "name": "Alice",
  "age": 31,
  "email": "alice@newdomain.com",
  "address": { "city": "Brooklyn", "zip": "10001" },
  "phone": "+1234567890"
}

工具会递归进入 address,并按路径排序报告以下条目:

路径类型详情
address.city已修改New York → Brooklyn
address.zip未修改10001
age已修改30 → 31
email已修改alice@example.com → alice@newdomain.com
name未修改Alice
phone新增+1234567890

汇总显示 1 项新增、0 项删除、3 项修改、2 项未修改。注意 address 本身没有被列为变更——只有它变更的子键被列出,这使输出聚焦于真正不同的地方。

会标记什么,会跳过什么

情况报告为
键只出现在右侧新增
键只出现在左侧删除
同键、不同的原始值已修改
同键、不同类型(对象对字符串)已修改,并显示两个值
同键、深度相等的值未修改
数组长度或元素不同已修改(整个数组)
数组元素相同且顺序一致未修改
键顺序不同无差异
重新缩进的输入无差异

最后两行正是对 JSON 使用结构化差异而非文本差异的原因:格式噪音消失了。

为什么不直接用文本差异?

按行的文本差异比较字符,因此每次键移动或缩进变化都会报告为改动。JSON 的键顺序没有意义,格式化工具又会自由改变空白,所以对两个语义相同的文档做文本差异,可能让整个文件都高亮。这个工具先解析再按值比较,从而去掉噪音。它在数组上做了相反的取舍:文本差异能显示插入了哪些行,而这个工具把整个数组报告为一个变更值。结构重要时用 JSON 差异,精确的字符编辑重要时用文本差异。

数组不够用时怎么办

因为数组是全有或全无,在大数组开头插入一个元素,会被报告为单条 changed 条目,展示整个旧数组和新数组。工具不会告诉你插入了 items[0],也不会告诉你 items[3].id 发生了变化。对小型数组这没问题,而且当元素没有稳定标识时,它避免了含糊的对齐。如果你需要元素级比较,请先规范化数据——例如把数组转成以 id 为键的对象——再对规范化后的形式做差异。

阅读输出与计数

四个计数器包含未修改的叶子,因此它们之和等于比较过的叶子总数,而不是差异数量。没有任何差异时,页面会显示简短的”无差异”信息,并仍然展示全部四个计数器。由于未修改条目也包含在列表里,一个大对象即使只有一个值变了也可能产生很长的结果;如果你只关心变动,浏览新增、删除、修改三种彩色标记即可。所有处理都在浏览器本地完成,API 响应和配置文件永远不会被传输。

相关工具

常见问题

为什么这个工具要求顶层必须是对象?

差异比对基于具名键和 `address.city` 这样的点号路径构建,因此要求两个输入的根节点都是 JSON 对象。顶层是纯数组、字符串、数字或 `null` 都会被拒绝,并给出明确的错误提示。如果需要比较数组,请把它包进对象里,例如 `{ "items": [...] }`。

数组元素是如何比较的?

数组被当作单个叶子值,用深度相等来判断。只要长度不同,或任意深度上有任一元素不同,整个数组都会报告为"已修改",并并排展示旧值和新值。工具不会报告 `items[2]` 这样的具体下标;元素级差异没有实现。

差异条目按什么顺序排列?

递归遍历结束后,路径会按语言环境感知的字符串规则做字母排序。这使输出是确定的,同样的输入永远得到同样的顺序。这也意味着 `address.city` 会排在 `age` 前面,因为比较是逐字符进行的,而不是按嵌套深度。

哪些内容计入"未修改"数量?

凡是两边都存在且深度相等的叶子都会计为未修改,包括嵌套键。当两边都是对象时,工具会递归进入,父路径本身不计入,只有叶子计数。新增的键、删除的键或修改过的值都不算未修改。

工具如何处理 null 与缺失键?

显式为 `null` 的键是存在的,会作为值参与比较;缺失的键则是 undefined。因此把字段从 `null` 改成字符串会报告为已修改,而新增一个恰好为 `null` 的字段会报告为新增。当 API 用 null 表示"无值"而不是省略字段时,这个区别很重要。

键的顺序或空白会影响结果吗?

不会。两个输入都用 JSON.parse 解析后按值比较,所以重新排列键顺序或改变缩进格式都不会产生差异。只有值、类型和存在性的真实变化才会被报告,这正是它适合处理版本控制里噪音差异的原因。

由 HandyTools Hub 团队审校 · 最后更新