JSON 工具箱

JSON 对比工具怎么选:文本 diff、结构 diff 与可视化

结论:文本 diff 负责找出「哪里不一样」,结构 diff 负责判断「内容是否等价」,可视化负责让人快速看懂差异。三层选对,才不会把键顺序当成事故。

进阶约 9 分钟发布于 2026-10-31更新于 2026-10-31
JSON 对比工具怎么选封面

结论:文本 diff 用于发现差异(先 jq -S 排序键再比),结构 diff 用于判断等价(数组按主键对齐),可视化只用于解释和复核;CI 里三层分工,不能混成一层。

两份 JSON 长得不一样,不代表它们的内容不一样;先选对比较的层次,再选工具。

一、三种「对比」其实不是一回事

很多人把「对比 JSON」当成一个动作,结果在不同场景里反复踩坑。至少要拆成三层:

层次 比较对象 能发现什么 典型场景
文本 diff 原始字节与行 缩进、键顺序、空格、换行 配置漂移、代码评审
结构 diff 解析后的对象与数组 键增删、值变化、数组元素变化 接口回归、数据迁移
可视化 diff 差异的呈现方式 差异落在哪一层、影响范围多大 人工复核、团队沟通

文本 diff 的优点是零依赖、速度快、和 Git 天然集成;缺点是它不知道 JSON 的语义。两份完全等价的文档,只要键顺序或缩进不同,就会显示成大片红绿。结构 diff 则相反:它先解析再比较,能识别「同一个值换了位置」,但需要选对数组对齐策略,否则会把一次插入误判成整段重写。

可视化不是第三种算法,而是把前两者的结果画出来。它适合让人快速确认「差异是不是预期的」,不适合替代自动化判断。把三层混在一起,最常见的结局是:CI 天天红,大家逐渐学会忽略它。

二、文本 diff:先用 jq -S 清掉键序噪音

如果只想快速看两份文件的差异,最省事的组合是 jq -S 加 diff -u。-S 会递归排序对象键,让「内容相同但键序不同」的文件先归一化:

jq -S . a.json > a.sorted.json
jq -S . b.json > b.sorted.json
diff -u a.sorted.json b.sorted.json

这一步解决的是最常见的假差异。RFC 8259 明确规定对象是无序集合,{"a":1,"b":2} 与 {"b":2,"a":1} 是同一个值;但文本 diff 会把它们全部标成变更。先排序再比较,红色区块才更接近真实问题。

注意 jq -S 只排序键,不排序数组。数组顺序在 JSON 里是有意义的,[1,2] 和 [2,1] 不是同一个值,所以工具不会替你做这个决定。若业务上数组本来就是无序集合,需要自己按主键排序后再比较。

还有一个容易忽略的细节:diff 在发现差异时退出码是 1,而被比较文件不存在、权限不足等错误也会返回非 0。在 CI 里不能简单地把「非 0」当成「有差异」,否则一次路径写错会被误报成内容变更。更稳的写法是先确认文件存在,再单独处理差异结果:

if ! diff -u a.sorted.json b.sorted.json; then
  echo "JSON 内容存在差异" >&2
  exit 1
fi

delta 让文本 diff 真正可读

原生 diff -u 的输出能用,但长行、深嵌套的 JSON 读起来很累。delta 是一个 diff 过滤器,可以给差异加上语法高亮、行号与更清晰的背景色:

diff -u a.sorted.json b.sorted.json | delta

它原本是 Git 的分页器,所以也能直接接管 git diff。对于「每天要看十次配置差异」的人,这类工具提升的是阅读速度,而不是算法精度。另一个常见选择是 git diff --no-index --word-diff,把比较粒度降到词一级,适合单行很长的压缩 JSON。

不过要提醒一句:美化不会消除假差异。如果键顺序没归一化,delta 只会把噪音显示得更漂亮。正确顺序永远是先规范化,再美化。

三、结构 diff:jsondiffpatch 才懂语义

需要判断「两份数据在语义上是否等价」时,应该用结构 diff。JavaScript 生态里最常用的是 jsondiffpatch,它输出的是一份描述变更的 delta 对象,而不是带 +、- 的文本行:

import { create } from 'jsondiffpatch';

const instance = create();
const delta = instance.diff(
  { id: 1, tags: ['a', 'b'], enabled: true },
  { id: 1, tags: ['a', 'c'], enabled: false },
);

console.log(JSON.stringify(delta, null, 2));

delta 的结构会区分「新增键」「删除键」「值被替换」「数组元素被移动」。这意味着你可以直接遍历 delta 做断言:没有差异时 delta 为 undefined,有差异时按路径逐条检查。和文本 diff 相比,它不会被缩进、键序、数字写法(1e3 与 1000)干扰。

同样重要的是,结构 diff 的结果可以直接接到补丁流程。jsondiffpatch 提供 patch 与 reverse,能把 delta 应用到原对象并撤销。但要注意:自动生成的补丁和你手工写的 JSON Patch 不是同一套格式,跨工具使用前需要转换。差异比较的完整思路可以对照 JSON 对比与差异。

四、键顺序造成的假差异怎么彻底消除

键顺序问题不只是「看起来烦」,它会直接制造错误结论。排查时按下面三条走:

  1. 比较前统一用 jq -S 或等价工具递归排序键;
  2. 删除每次都会变化的字段,例如 timestamp、requestId、traceId;
  3. 确认没有重复键。重复键在解析阶段就已被「后者覆盖」,文本 diff 能看到,结构 diff 看不到。
jq -S 'del(.meta.timestamp, .meta.requestId)' a.json > /tmp/a.json
jq -S 'del(.meta.timestamp, .meta.requestId)' b.json > /tmp/b.json
diff -u /tmp/a.json /tmp/b.json

这套流程适合配置漂移排查。把 staging 与 production 的配置都过一遍,剩下的差异才是真正需要解释的。动态字段的过滤规则最好写进脚本并纳入版本控制,而不是每次靠记忆手敲。编辑与脚本侧的配套工具可以看 编辑器、命令行与自动化工具链。

五、数组到底按索引还是按 id

数组是 JSON 对比里最容易出错的部分。默认按索引对齐时,在开头插入一条记录,后面所有元素都会「错位」,工具会把它们逐个标成变更。这不是工具坏了,而是对齐策略选错了。

对齐方式 适用前提 优点 风险
按索引 数组顺序有业务含义 简单、无需额外字段 一次插入造成连锁误报
按主键 元素是带唯一 id 的集合 能识别插入、删除、移动 需要稳定且唯一的主键
最长公共子序列 顺序重要但允许中间插入 比纯索引更稳 结果不易解释、性能开销更大

jsondiffpatch 用 objectHash 指定主键,数组元素会先建索引再比较:

const instance = create({
  objectHash: (item: { id?: string; sku?: string }) => item.id ?? item.sku,
});

如果只用命令行的 jq,可以先排序再走文本 diff:

jq -S 'sort_by(.id)' items.json > items.sorted.json

按 id 对齐的代价是「位置变化」不再算差异。如果业务需要知道某条记录从第 2 位挪到了第 5 位,就必须额外比较顺序字段,不能只看集合内容。

六、可视化对比:给人看的那一层

结构 diff 的输出适合机器,不适合开会。需要向别人解释差异时,可视化工具更有效:

  • VS Code 的 diff 编辑器适合逐文件核对,左右并排、行内高亮;
  • JSON Crack、JSON Hero 这类树形查看器适合看差异落在哪一层;
  • jsondiffpatch 自带 HTML formatter,可以把 delta 渲染成可折叠的富文本视图。

可视化工具的价值在「定位上下文」:文本 diff 告诉你第 88 行变了,树形视图告诉你第 88 行属于 data.items[3].price。但可视化通常需要把完整文档加载进浏览器或桌面端,处理生产报文前要先确认它是本地计算还是上传到服务器。

七、把对比放进 CI 的三条实践

第一,先规范化再比较。固定缩进、递归排序键、过滤动态字段,三步做完再进入 diff。否则 CI 会因为无关噪音反复失败。

第二,按语义选对齐方式。数组是集合就用主键,数组是序列就保留索引,不要用一套规则套所有接口。

第三,断言值而不是断言文本。测试里比较解析后的对象,比比较 JSON.stringify 的结果稳定得多。需要输出失败信息时,只打印差异路径和前后值,不要把整份报文倒进日志。

# 只输出差异路径与前后值,避免日志被大文件淹没
jq -S 'del(.timestamp)' actual.json > /tmp/actual.json
jq -S 'del(.timestamp)' expected.json > /tmp/expected.json
diff -u /tmp/expected.json /tmp/actual.json | head -n 80

如果差异需要长期追踪,可以把规范化脚本和期望文件一起入库;如果只是一次性排查,临时脚本用完就删,别让它悄悄变成第二套业务逻辑。

八、工具选择表

场景 首选 备选 关键提醒
偶尔看两份文件 VS Code diff diff -u 先格式化,减少噪音
配置漂移排查 jq -S + diff delta 过滤动态字段
接口响应回归 jsondiffpatch deepdiff 数组按主键对齐
给团队讲解差异 JSON Crack / JSON Hero HTML formatter 注意数据是否上传
超大文件 jq 抽取子树 jless 浏览 不要整棵加载进内存
Git 提交前检查 git diff --no-index pre-commit 退出码要单独处理

没有「最好的 JSON 对比工具」,只有「当前这层该用什么工具」。文本 diff 负责发现变化,结构 diff 负责判断等价,可视化负责解释变化。三层各司其职,比找一个全能工具靠谱得多。跨分支合并时的冲突处理,可以继续看 JSON 与 Git 合并。

FAQ

jq -S 会不会把数组顺序也改掉?

不会。-S 只对对象的键排序,数组保持原有顺序。JSON 里数组是有序的,工具不会替你把 [1,2] 变成 [2,1];如果业务上数组是无序集合,需要自己用 sort_by 明确排序。

1 和 1.0 在结构 diff 里算同一个值吗?

大多数解析器会把它们都解析成同一个数字,因此结构 diff 通常认为相等;文本 diff 则会标成差异。判断「是否等价」要用结构 diff,判断「文件是否逐字节一致」才用文本 diff。

对比结果能直接当 JSON Patch 用吗?

不能直接替换。对比工具输出的是「哪里不一样」,JSON Patch 要求的是「按顺序执行哪些操作」,两者格式和语义都不同。自动生成的补丁还常用数组索引定位,并发场景下索引会失效,应用前建议加 test 校验。

几十 MB 的 JSON 怎么对比?

先抽取关心的子树,例如 jq '.data.items',再按主键排序后比较;不要一上来就把整棵树加载进内存做递归 diff。超大文件更适合流式处理或分片比较,内存与耗时的量级需要单独评估。

在线对比工具会不会泄露数据?

取决于实现。纯前端工具在浏览器里计算,数据不上传;服务端渲染的工具会把内容发到后端,可能进入日志。对比生产配置或用户报文前,先确认页面是否本地计算,必要时直接断网测试一次。

如何决定先用文本 diff 还是结构 diff?

只想快速看两份文件哪里不同,先用 jq -S 排序键再跑 diff -u;要判断两份数据在语义上是否等价,用 jsondiffpatch 这类结构 diff,并按主键对齐数组。文本 diff 零依赖、和 Git 天然集成,结构 diff 不会被缩进、键序和数字写法干扰。

为什么 CI 里不能把 diff 的非零退出码都当成有差异?

diff 发现差异时退出码是 1,但文件不存在、权限不足等错误也返回非 0。CI 里简单地把非 0 当成「有差异」,会把一次路径写错误报成内容变更。更稳的写法是先确认文件存在,再单独处理差异结果。

是否需要在浏览器里对比几十 MB 的文件?

不建议。几十 MB 的文件应先用 jq 抽取关心的子树,再按主键排序后比较;整棵树加载进内存做递归 diff,内存与耗时都不划算。超大文件更适合流式处理或分片比较。