← 返回博客

如何比较两个 JSON 文件的差异

2026-04-17 · 5 分钟阅读

← 返回博客

如何比较两个 JSON 文件的差异

· 5 分钟阅读

JSON 比较的核心难点

JSON 文件的比较比普通文本文件更复杂,原因在于 JSON 的语义等价性不等同于字符串等价性。两个 JSON 对象即使字段顺序不同,在语义上可能完全等价({"a":1,"b":2}{"b":2,"a":1} 是同一个 JSON 对象)。但如果用普通的文本 diff 工具比较,它们会被认为是不同的——这会产生大量误报的"差异"。

同样,格式化方式不同的 JSON(缩进、换行、空格)在文本层面不同,但语义完全相同。这就是为什么需要专门的 JSON-aware diff 工具,而不是简单地用 diff 命令对比两个文件。理解这个区别,是选择正确比较方法的前提。

使用 jq 进行命令行对比

jq 是命令行 JSON 比较最强大的工具。结合 jq -S(对键名排序)和标准 diff,可以实现忽略字段顺序的语义比较:

# 基本语义比较(忽略字段顺序和格式差异)
# Semantic comparison (ignores field order and formatting differences)
diff /dev/null; then
  echo "JSONs are identical"
else
  echo "JSONs differ"
fi

JavaScript 中的 JSON 深比较

在 JavaScript/Node.js 中,直接用 === 比较两个对象只比较引用,不比较内容。需要深度比较(deep equal)来确认两个 JSON 值是否语义等价:

// 使用 JSON.stringify + 排序键(简单场景)
// Using JSON.stringify + sorted keys (simple scenarios)
function jsonEqual(a, b) {
  return JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b));
}

function sortKeys(obj) {
  if (Array.isArray(obj)) return obj.map(sortKeys);
  if (obj && typeof obj === 'object') {
    return Object.fromEntries(
      Object.entries(obj).sort().map(([k, v]) => [k, sortKeys(v)])
    );
  }
  return obj;
}

// 使用 fast-deep-equal 库(推荐,处理边缘情况)
// Using fast-deep-equal library (recommended, handles edge cases)
import equal from 'fast-deep-equal';
console.log(equal(obj1, obj2)); // true/false

// 使用 deep-diff 库获取详细差异
// Using deep-diff library for detailed differences
import { diff } from 'deep-diff';
const differences = diff(obj1, obj2);
// differences 是变更数组,每个元素包含路径、类型、旧值、新值

Python 中的 JSON 比较

Python 比较 JSON 非常简洁,因为 Python 的字典比较天然忽略键的顺序:

import json

# 加载并比较(Python dict 比较忽略键顺序)
# Load and compare (Python dict comparison ignores key order)
with open('file1.json') as f:
    obj1 = json.load(f)
with open('file2.json') as f:
    obj2 = json.load(f)

print(obj1 == obj2)  # True if semantically equal

# 获取详细差异
# Get detailed differences
def json_diff(obj1, obj2, path=""):
    if type(obj1) != type(obj2):
        print(f"{path}: type changed {type(obj1).__name__} → {type(obj2).__name__}")
        return
    if isinstance(obj1, dict):
        for key in set(obj1) | set(obj2):
            if key not in obj1:
                print(f"{path}.{key}: added = {obj2[key]}")
            elif key not in obj2:
                print(f"{path}.{key}: removed = {obj1[key]}")
            else:
                json_diff(obj1[key], obj2[key], f"{path}.{key}")
    elif isinstance(obj1, list):
        for i, (a, b) in enumerate(zip(obj1, obj2)):
            json_diff(a, b, f"{path}[{i}]")
        if len(obj1) != len(obj2):
            print(f"{path}: length changed {len(obj1)} → {len(obj2)}")
    elif obj1 != obj2:
        print(f"{path}: {obj1!r} → {obj2!r}")

API 响应比较:测试中的用法

在 API 测试中,比较响应 JSON 是一项常见需求:验证接口在重构前后行为一致,或者对比不同环境(测试/生产)的响应差异。通常需要忽略动态字段(如时间戳、请求 ID)后再比较:

# 使用 jq 忽略动态字段后比较两个 API 响应快照
# Using jq to ignore dynamic fields before comparing two API response snapshots
IGNORE_FIELDS='del(.meta.timestamp, .meta.requestId, .data.updatedAt)'

diff \
   /tmp/old.json
git show HEAD:config.json | jq -S . > /tmp/new.json
diff /tmp/old.json /tmp/new.json

JSON Patch:描述 JSON 差异的标准格式

RFC 6902 定义了 JSON Patch 格式,用于描述对 JSON 文档进行的一系列操作(add、remove、replace、move、copy、test)。这不仅是一种表示"差异"的标准方式,还可以作为 API 的 PATCH 请求格式,精确描述客户端希望对资源做的修改:

在需要精确追踪和回放 JSON 变更历史的场景(如协同编辑、配置变更审计),JSON Patch 比简单的"前后对比"更有用:你可以将一系列 Patch 操作逐步应用,实现精细的版本控制和变更回滚。配合 RFC 7396 的 JSON Merge Patch(更简单的部分更新格式),可以满足大多数 API PATCH 操作的需求。

// JSON Patch 示例 / JSON Patch example
[
  { "op": "replace", "path": "/user/name", "value": "Bob" },
  { "op": "add", "path": "/user/phone", "value": "+1-555-0100" },
  { "op": "remove", "path": "/user/legacy_field" }
]

// 使用 fast-json-patch 库应用 Patch
import jsonpatch from 'fast-json-patch';
const patched = jsonpatch.applyPatch(originalDoc, patchOps).newDocument;

// 生成两个对象之间的 Patch
const patch = jsonpatch.compare(originalDoc, modifiedDoc);

立即尝试在线工具,无需安装,免费使用。

打开工具 →

立即免费使用相关工具

免费使用 →