博客

JSON比较(Diff)完全指南:找出两个JSON文档的差异

比较两个JSON文档听起来简单,直到你遇到键的重新排序、嵌套数组变化和空白符噪音。一个简单的文本diff会将每个移动的键都报告为删除和插入,即使数据完全相同。本指南涵盖何时需要JSON比较、不同方法及其取舍,以及在开发工作流中有效使用JSON diff的实用技巧。

何时需要JSON比较

JSON diff不仅仅是一个便利功能。它解决了专业开发中经常遇到的真实问题。

API版本控制与变更检测

当第三方API发布新版本时,你需要确切知道发生了什么变化。将v1响应样本与v2响应样本进行比较,可以得到精确的变更报告:哪些字段被添加、删除、重命名或改变了类型。这比阅读可能不完整或不准确的更新日志文档要可靠得多。

配置追踪

package.jsontsconfig.jsonappsettings.json 以及自定义应用配置等配置文件会随时间积累变更。比较两个不同部署或Git版本的配置,帮助你了解正常环境与故障环境之间发生了什么变化。

测试验证

在快照测试和集成测试中,预期输出通常是一个JSON文档。当测试失败时,JSON diff能立即展示哪些字段发生了变化以及变化幅度——而不仅仅是告诉你断言失败了。

数据库记录审计

当应用更新一条记录时,在审计日志中存储修改前后的JSON diff,可以得到一份紧凑、人类可读的历史记录,精确反映什么内容发生了变化、谁进行了修改以及修改时间。

基于文本的比较与结构化比较

比较JSON有两种根本不同的方法,选错方法会产生大量噪音。

基于文本的Diff

文本diff(如 git diff 或Unix的 diff 命令)将JSON视为纯文本按行比较。它速度快且不需要JSON解析,但会将任何格式变化都视为差异。

如果相同的数据使用不同的缩进格式化,或者对象的键顺序不同,文本diff会报告大量差异,即使JSON的值完全相同:

// "修改前"(键按某种顺序排列)
{
"name": "Alice",
"id": 42
}
// "修改后"(键重新排序)
{
"id": 42,
"name": "Alice"
}

文本diff会报告两个键都发生了变化。结构化diff则报告无差异。

结构化Diff

结构化diff先将两个JSON文档解析为内存中的表示,再比较解析后的结构。这意味着:

  • 对象中键的顺序无关紧要——{"a":1,"b":2} 等同于 {"b":2,"a":1}
  • 空白符和缩进完全被忽略
  • 只报告实际的值差异

对于比较JSON数据,结构化diff几乎总是正确的选择。

比较维度文本Diff结构化Diff
键重新排序报告为变更忽略
空白符变化报告为变更忽略

类型变化("1" vs 1

报告报告
嵌套值变化报告(含噪音)报告(干净)
要求有效JSON
大文件处理速度更快较慢(解析开销)

处理键顺序差异

根据RFC 8259,JSON对象被定义为键值对的无序集合。但实际上,不同的语言、序列化器和格式化工具对相同数据产生不同的键顺序。

Python的 json.dumps 按插入顺序输出键。JavaScript的 JSON.stringify 在现代引擎中保持插入顺序,但历史上行为有所不同。Go的 encoding/json 按结构体字段声明顺序序列化。当这些系统交换数据时,生成的JSON在语义上相同,但在文本上不同。

正确的JSON diff会在比较前规范化键顺序,因此 {"z":1,"a":2}{"a":2,"z":1} 会被报告为相等。

如果你使用文本diff并希望处理键重新排序,可以在比较前对键排序:

import json
def normalize(data):
return json.dumps(json.loads(data), sort_keys=True, indent=2)
# 然后对规范化后的版本进行文本diff

数组比较的挑战

数组是JSON比较中最难的部分。与对象不同,数组是有序的——[1, 2, 3][3, 1, 2] 在语义上是不同的,即使它们包含相同的元素。这带来了两个常见问题。

插入和删除引起的位移

当在数组开头或中间插入一个元素时,简单的diff会报告此后每个元素都发生了变化,因为它们的索引发生了偏移。如果只在索引0处插入了一个元素,却显示第0到50项都被修改,这样的diff毫无意义。

更好的diff算法(如Myers diff)能检测公共子序列,正确报告插入和删除,从而最小化误报。

识别对象数组中的元素

对于对象数组,优秀的diff工具通过有意义的键(如 id)而非数组位置来识别对应元素。这样,列表中元素的重新排序会被正确报告为移动,而非一组删除和插入。

// 修改前
[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
// 修改后(顺序互换)
[{"id": 2, "name": "Bob"}, {"id": 1, "name": "Alice"}]

基于位置的diff报告两处变化。基于ID的diff报告无变化。

开发中JSON Diff的实用技巧

比较前先规范化

在进行diff之前,始终规范化两个文档:排序键、规范化数字格式、去除修饰性空白符。这消除了噪音,使diff输出有意义。

在CI/CD流水线中使用JSON Diff

在CI流水线中自动化JSON比较,有助于在合并前捕获对API契约或配置文件的意外更改。

# 在Shell脚本中比较API响应快照
npx json-diff expected.json actual.json
if [ $? -ne 0 ]; then
echo "API响应发生意外变化"
exit 1
fi

快照测试

Jest等框架使用JSON序列化进行快照测试。当快照测试失败时,输出实际上就是一个JSON diff。从结构角度阅读它——注意哪些键发生了变化,而不只是哪些行变了——会让修复失败的快照快得多。

// Jest快照测试——失败输出是可读的JSON diff
expect(apiResponse).toMatchSnapshot();

追踪不同环境间的配置漂移

如果你有staging和production配置,它们应该几乎相同,定期进行diff可以发现无意中引入的环境特定覆盖。

比较前脱敏敏感字段

在为调试或日志记录共享diff时,比较前先对敏感字段进行脱敏处理。构建一个规范化步骤,将令牌、密码和个人信息替换为占位符值。

function redact(obj, keys) {
const result = { ...obj };
for (const key of keys) {
if (key in result) result[key] = '[REDACTED]';
}
return result;
}

读懂JSON Diff输出

标准JSON diff使用颜色和符号来表示变化:

符号 / 颜色含义

+ 绿色

新增 — 存在于新文档,不存在于旧文档

- 红色

删除 — 存在于旧文档,不存在于新文档

~ 黄色

修改 — 键在两个文档中都存在,但值发生了变化
无符号未变更 — 在两个文档中完全相同

阅读diff时,从修改字段(~)开始,因为这些通常是最值得关注的。新增和删除更清晰——某个内容被明确添加或移除了。

总结

JSON diff是API开发、配置管理和数据审计的必备工具。基于文本的diff速度快,但键重排和格式变化会产生噪音。结构化diff才是比较JSON数据的正确工具——它先解析文档,再比较实际值。

JSONKit的比较工具在浏览器中提供并排的结构化JSON diff。粘贴两个JSON文档,差异会内联高亮显示,无需任何配置。所有比较均在客户端完成——你的数据留在浏览器中。