Why JSON Key Order Changes a Diff: Structural Comparison
Two JSON texts can differ only by object-member order while representing the same data model for a given comparison policy. This article covers object-key order, array order, and structural comparison. It does not cover Myers sequence diff.
1. Problem: text equality is not structural equality
{"a":1,"b":2} and {"b":2,"a":1} have different bytes, so direct string equality fails. Parse and compare structure only when the domain treats object members as a mapping; do not discard order when raw text is signed, cached, or displayed as meaningful output.
2. Minimal reproduction: sort objects, not arrays
A recursive key sort makes the two objects stable, but arrays stay ordered, so [1,2] and [2,1] remain different.
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()); // false3. Root cause: JSON text keeps order; policy supplies meaning
A parser reads members in text order and JavaScript has property-enumeration rules, but neither is a business comparison policy. A structural JSON diff normally treats objects as name-to-value mappings and arrays as ordered sequences. Duplicate member names are not a stable basis for interoperable comparison because parsers can handle them differently.
4. Recommended policy: canonicalize only plain JSON objects
For acyclic JSON values, recursively sort object keys and recurse into array elements without reordering them. Compare the resulting canonical text or create a path diff from it. This is unsuitable when insertion order is a product semantic, and it does not replace domain rules for numbers or dates.
5. Complete code: stable JSON representation
This TypeScript runs in browsers and Node.js. It rejects non-finite numbers so JSON.stringify cannot silently turn them into null.
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 {
return JSON.stringify(canonicalize(JSON.parse(leftText) as Json)) === JSON.stringify(canonicalize(JSON.parse(rightText) as Json));
}
console.log(sameJsonObjectShape('{"b":2,"a":1}', '{"a":1,"b":2}')); // true
console.log(sameJsonObjectShape('{"items":[1,2]}', '{"items":[2,1]}')); // false6. Incorrect approaches
Sorting characters corrupts strings and escapes. Sorting arrays hides list-order changes unless the domain explicitly defines the array as a set. JSON.stringify(JSON.parse(text)) alone also preserves insertion order rather than producing a common key order.
7. Boundaries: numbers, null, and duplicate names
JavaScript parses 1 and 1.0 to the same Number; compare text or tokens if spelling matters. Null is different from a missing field. Reject duplicate object names, report them, or define an explicit parser policy before comparison.
8. Verification
Test reordered keys, nested reordered keys, reordered arrays, null versus missing, and alternate number spellings. The first two should match, while the rest follow the documented domain policy. Do not apply this rule blindly to signed raw HTTP text.
9. FAQ
Does sorting rewrite my file? Not here; it produces an in-memory comparison form.
Can an array ignore order? Only when the domain defines it as a set and also defines identity and duplicates.
Can I use a deep-equality library? Yes, after verifying its object and array semantics match the JSON contract.
10. Summary
Key order creates a text difference, not automatically an object change. Canonicalize object keys recursively and preserve array order for an explicit, testable comparison policy. Document array, null, and raw-text requirements before relying on it.
Sources and further reading
Specifications and references for checking the article’s technical details.
Related articles
API Contract Breaking Changes: Checking Field Changes
Check response-JSON contract changes by separating added, removed, typed, required, and enum fields, with a self-contained TypeScript example and release verification steps.
How it worksMyers Diff for JSON Arrays: Limits and Implementation
Use Myers shortest edit scripts for ordered JSON arrays, with insert/delete/equal operations, path patches, and clear limits around object identity and duplicate values.
TroubleshootingJSON to SQL Types: MySQL and PostgreSQL
Generate conservative MySQL and PostgreSQL candidates for JSON, numbers, strings, arrays, objects, and NULL while explaining why samples cannot replace an explicit schema.
Keep exploring
Use the related browser tool to test the workflow with your own sample data.
Open related tool