Javascript is required
错误排查发布于 2026-07-28审校于 2026-08-086 分钟阅读

JSON 对象键顺序不同为何 Diff:结构化比较方法

两段 JSON 文本仅交换对象成员顺序时,字符串比较会显示差异;这不必然意味着数据模型改变。本文只讨论对象键顺序与结构化比较策略:何时排序对象键、为什么不能排序数组,以及比较前应先定义的语义。本文不讨论 Myers 序列算法。

JSON DiffObject KeysCanonicalizationComparison

一、问题概述:文本相等与结构相等不是同一个判断

{"a":1,"b":2}{"b":2,"a":1} 的字节序列不同,因此直接 === 会失败。对配置快照、接口样例或审计数据,如果业务只关心对象成员名和值,应先解析再比较结构;如果签名、缓存键或展示顺序依赖原始文本,则不能擅自忽略顺序。

二、最小复现:对象可排序,数组不能

递归排序对象键后,两段对象会得到相同规范文本;数组元素仍保持原顺序,因此 [1,2][2,1] 仍不同。

const left = '{"b":2,"a":1}';
const right = '{"a":1,"b":2}';

console.log(left === right); // false
console.log(JSON.stringify({ a: 1, b: 2 }) === JSON.stringify({ b: 2, a: 1 })); // false
console.log([1, 2].toString() === [2, 1].toString()); // false

三、根因:JSON 文本保留顺序,比较策略需要自行定义

JSON 解析器会按文本顺序读取成员,JavaScript 也有自己的属性枚举规则;这些实现细节不等于业务比较规则。结构化 diff 应明确对象被看作名称到值的映射,数组被看作有序序列。若 API 允许重复对象成员名,不同解析器可能采取不同结果;这类输入不适合作为稳定比较基础。

四、推荐方案:只规范化普通对象键

对无循环引用、可表示为 JSON 的值,可递归按键排序普通对象,数组只递归处理元素但不重排。然后比较规范后的 JSON 文本,或在规范化结果上生成路径级 diff。此策略不适用于把对象键插入顺序作为产品语义的格式,也不能替代数字精度和日期语义的专门规则。

五、完整代码:生成稳定的 JSON 表示

以下 TypeScript 可在浏览器或 Node.js 运行。它拒绝非有限数字,避免 JSON.stringify 将 NaN 和 Infinity 静默改成 null;输入必须是普通 JSON 值。

type Json = null | boolean | number | string | Json[] | { [key: string]: Json };

function canonicalize(value: Json): Json {
  if (typeof value === "number" && !Number.isFinite(value)) {
    throw new TypeError("JSON numbers must be finite");
  }
  if (value === null || typeof value !== "object") return value;
  if (Array.isArray(value)) return value.map(canonicalize);

  const result: { [key: string]: Json } = {};
  for (const key of Object.keys(value).sort()) {
    result[key] = canonicalize(value[key]);
  }
  return result;
}

function sameJsonObjectShape(leftText: string, rightText: string): boolean {
  const left = canonicalize(JSON.parse(leftText) as Json);
  const right = canonicalize(JSON.parse(rightText) as Json);
  return JSON.stringify(left) === JSON.stringify(right);
}

console.log(sameJsonObjectShape('{"b":2,"a":1}', '{"a":1,"b":2}')); // true
console.log(sameJsonObjectShape('{"items":[1,2]}', '{"items":[2,1]}')); // false

六、常见错误方案

直接对原始字符串排序字符会破坏字符串字面量和转义。把数组也排序会掩盖列表顺序变化,除非业务明确把该数组定义为集合且另有去重/排序规则。仅使用 JSON.stringify(JSON.parse(text)) 也不能保证两段对象采用同一键顺序,因为它保留解析后对象的插入顺序。

七、边界条件:数字、null 与重复成员名

11.0 解析到 JavaScript Number 后都表示相同数值;若原始数字写法本身重要,应比较文本或保留词法 token。null 与缺失字段不同,规范化不能把它们合并。JSON 对象的重复成员名会降低互操作性,比较工具应拒绝它、报告它,或在协议中明确解析规则。

八、如何验证比较规则

准备键序不同、嵌套对象键序不同、数组换序、null 与缺失字段、不同数字写法五类输入。断言前两类相等,后三类按团队规则不同。再将该规则应用到同一类型的样例数据,而不是把它误用于带签名的原始 HTTP 文本。

九、FAQ

问:对象键排序会改变原始文件吗?答:这里只在内存中生成比较表示;是否回写需要独立格式化策略。

问:数组能否忽略顺序?答:只有领域模型把该数组定义为集合时才可以,并应说明元素身份和重复值处理。

问:为什么不直接用深度相等库?答:可以,但仍要确认该库对对象、数组、NaN 和原型的语义是否与 JSON 协议一致。

十、总结

键顺序造成的是文本差异,不必然是对象语义变化。把对象键递归规范化、保留数组顺序,能让比较规则可验证且可解释。比较前仍要定义数组是否有序、null 是否不同于缺失字段,以及原始文本是否承担签名或展示职责。

来源与延伸阅读

技术审校所依据的规范与权威参考资料。

相关文章

继续阅读

可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。

打开关联工具