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.
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}
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
- Paste or open the two versions. The old one on the left, the new one on the right. Both must be valid JSON.
- Decide how arrays should be compared. Leave order on for ordered lists; tick Ignore the order of array items for sets.
- 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}
| 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)
-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
- RFC 8259, section 1. An object is an unordered collection of name/value pairs
- jq manual. --sort-keys / -S sorts the fields of each object
Originally published on ThatToolSite.
Top comments (3)
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 unorderedtagsand orderedsteps; 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.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.
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.