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

结论:文本 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 对比与差异。
四、键顺序造成的假差异怎么彻底消除
键顺序问题不只是「看起来烦」,它会直接制造错误结论。排查时按下面三条走:
- 比较前统一用
jq -S或等价工具递归排序键; - 删除每次都会变化的字段,例如
timestamp、requestId、traceId; - 确认没有重复键。重复键在解析阶段就已被「后者覆盖」,文本 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,内存与耗时都不划算。超大文件更适合流式处理或分片比较。