DEV Community

ThatToolSite
ThatToolSite

Posted on Originally published at thattoolsite.com

How to Compare Two JSON Files (Ignoring Key Order)

JSON diff compares two JSON documents by their data, ignoring key order and formatting, and lists every change by path. Both documents stay in your browser.

Compare JSON

Why a text diff gives the wrong answer

A JSON object is, in the words of the standard, an unordered collection of names and values. These two documents hold exactly the same data:

{"id": 7, "name": "Ada", "tags": ["admin", "editor"], "plan": "pro"}
{"tags": ["admin", "editor"], "name": "Ada", "plan": "pro", "id": 7}
Enter fullscreen mode Exit fullscreen mode

A text diff compares characters, so it marks the whole line as changed. Pretty-printed, every line would differ. Real changes hide among fake ones, and a reformatted file looks completely rewritten. Comparing by structure fixes this: each value is matched by its path, such as $.plan or $.items[2].price, and only real changes are reported. JSON diff says “No differences” for the pair above.

Compare two JSON files by structure

  1. Paste or open the two versions. The old one on the left, the new one on the right. Both must be valid JSON.
  2. Decide how arrays should be compared. Leave order on for ordered lists; tick Ignore the order of array items for sets.
  3. Read the list of changes. Each line shows the path, the old value and the new value. Copy the summary for a ticket or a review.

If a file won't parse, fix it first in the JSON validator, which shows the line and column of the problem.

Arrays: an ordered list or a set?

Key order never matters in JSON, but array order can. Take document A from above and this new version, where the plan changed and the tags were listed the other way round:

{"plan": "team", "tags": ["editor", "admin"], "name": "Ada", "id": 7}
Enter fullscreen mode Exit fullscreen mode
Setting JSON diff reports
Order matters (default) 3 differences: $.tags[0] “admin” removed, $.tags[1] “admin” added, $.plan “pro” → “team”
Ignore the order of array items 1 difference: $.plan “pro” → “team”

Both answers are right; they answer different questions. Tags, permissions and IDs are usually sets, so ignore order. Steps of a process, ranked results or the lines of an order are lists, so keep it. For a list of records that should be matched by an id field, sort both arrays by that field first, then compare with order on.

Is 1 the same as "1" or 1.0?

JSON diff compares values by type and by numeric value. That catches the changes that actually break programs, while ignoring ones that don't:

Old New Reported?
"id": 1 "id": "1" Yes: a number became a string
"zip": "02134" "zip": 2134 Yes: and the leading zero is lost
"total": 31.5 "total": 31.50 No: the same number written differently
12345678901234567890 12345678901234567891 Yes: compared exactly, not rounded

The last row matters for IDs. Many JavaScript-based tools round both numbers to the same value and call them equal.

Comparing JSON on the command line

With jq installed, you can sort keys and then use an ordinary diff:

diff <(jq -S . old.json) <(jq -S . new.json)
Enter fullscreen mode Exit fullscreen mode

-S sorts the keys of every object. It does not sort arrays, so reordered arrays still show up as changes. Sort a list of records by a field with jq 'sort_by(.id)' before comparing. In Python, json.load(a) == json.load(b) answers yes or no, ignoring key order, but doesn't say what changed.

Timestamps, IDs and other noise

Two API responses often differ in fields that change on every call: updatedAt, request IDs, tokens. They drown out the changes you care about. Remove them from both documents before comparing, for example with jq 'del(.updatedAt, .requestId)', or by deleting the lines by hand in a copy.

For comparing code, config written as YAML or any other text where order and whitespace matter, use the diff checker instead.

Sources


Originally published on ThatToolSite.

Top comments (3)

Collapse
 
launchgatecheck profile image
Launch Gate •

The exact-integer example is useful. I'd add a duplicate-count fixture beside the array-order toggle: [1,1,2] versus [1,2,2]. "Ignore order" could mean multiset comparison (different) or true set comparison (equal), so naming that choice would make the result easier to interpret. Does the toggle apply recursively to every array? A response can contain unordered tags and ordered steps; a global switch would need a visible caveat there. Not reporting a bug, just two cases I'd want in the examples before using the summary in a review.

Collapse
 
thattoolsite profile image
ThatToolSite •

Thanks, both are good points, and I checked the code to answer exactly.

Duplicates: "ignore order" is a multiset comparison, not a set comparison. Each item is matched once, so [1,1,2] vs [1,2,2] is reported as different (one 1 removed, one 2 added). If you want true set semantics, dedupe both sides first, e.g. jq '.tags |= unique'.

Recursion: yes, the toggle applies to every array at every depth. It's one global switch, so in a response with unordered tags and ordered steps it would also ignore the order of steps. The workaround for now is to compare with order on and read the moved items as reorders, or to sort just the unordered arrays before comparing (jq '.tags |= sort').

You're right that both should be stated next to the toggle rather than left to guess. I'll add the duplicate example and the "applies to all arrays" caveat to the tool and the guide.

Collapse
 
launchgatecheck profile image
Launch Gate •

That pins it, and "matched once" is the right phrasing for the docs - it states the algorithm, not just the property. The tags-and-steps response is also the ready-made fixture for whatever replaces the global switch: if per-path control ever lands, the regression test writes itself. Same response twice: reorder tags, assert no diff; reorder steps, assert a diff. Until then the order-on workaround has a noise cost worth knowing: in a payload where most arrays are genuinely unordered, reading every reorder as a diff buries the one move that matters, which is exactly why your jq-sort suggestion is the better interim answer. Thanks for checking the code instead of guessing.