How to Compare JSON Files: Structural vs Text Diff

🔀 High-volume dev query💰 CPC: $1–3⏱️ 7 min read

"What changed between these two configs?" should take five seconds. With JSON it routinely takes longer, because the obvious tool, a line diff, wasn't built for a data format where key order doesn't matter and one reformat rewrites every line. Here's how to get a diff that tells the truth.

Advertisement

Why a plain text diff lies about JSON

Three properties of JSON break line-based comparison. Key order is insignificant per the spec, so {"a":1,"b":2} and {"b":2,"a":1} are the same document, but a text diff flags both keys as changed. Whitespace is insignificant, so minified versus pretty-printed versions of the same file show as one giant change. And arrays have identity by position only, so an item that moved from index 2 to index 0 appears as a delete here and an add there, dragging neighboring lines with it.

None of this is the text diff's fault. It compares characters faithfully. The problem is that you care about the document the characters encode, not the characters.

The command-line fix: normalize, then diff

If you live in a terminal, jq plus diff covers most cases in one line:

diff <(jq -S . a.json) <(jq -S . b.json)

The -S flag sorts object keys and jq pretty-prints consistently, which neutralizes the two big liars, key order and formatting. What survives is real value change plus array-position noise. For CI-style checks you'll want exit codes and maybe jd or json-diff from npm for structural output, but the jq one-liner is the fastest answer for two files already on disk.

What a structural diff adds

A structural diff parses both documents first, then compares the resulting trees. Objects walk key by key, so a changed value is one entry with the old and new value, no context lines. Arrays pair items by deep equality: items that exist in both but at different indexes report as moves with from and to positions, items only in the original report as removals, and items only in the changed file as additions. The same approach powers the classic MIT-licensed libraries (jsondiffpatch, json-diff) that got this problem named.

Changejq + diff showsStructural diff shows
Key reorderedNothing (after -S)Nothing
ReformattedNothing (after jq)Nothing
Item moved in arrayDelete + add pairMoved 2→0
Value changedChanged lineModified entry, old → new
Duplicate values reshuffledNoiseMoves, or nothing with ignore-order

One honest limitation worth knowing: when an array item's contents change, the old and new versions are no longer equal, so they can't pair, and the diff reports a removal plus an addition. Deducing that {"qty":2} became {"qty":3} for "the same" item requires guessing identity, usually via an id field, and guessing is exactly what a correct diff refuses to do without being told.

Diff JSON in your browser

Paste two documents; get additions, removals, modifications, and array moves as JSON paths, with an ignore-array-order toggle. Nothing is uploaded.

JSON Diff →

Reading the output: paths are the interface

Good structural diffs report locations as paths: $.customer.email for an object field, $.items[1] for an array slot, and bracket form $["weird key!"] when a key contains characters dot-syntax can't survive. Paths turn a diff from something you read into something you can act on, they map directly onto JSON Patch operations and jq selectors, and they're compact enough to paste into a pull-request comment. When you review someone's API change, the path list is usually all you need.

Picking the right tool for the job

Two documents on disk, quick sanity check: the jq one-liner. Reviewing an API response or config change where order matters: a structural diff with move detection. Comparing dumps where array order is meaningless (tag lists, ID sets): structural diff with the ignore-order option. And when the files aren't actually JSON, CSVs or logs or code, a plain text diff is still correct, and our diff checker does that without ceremony. The failure mode to avoid is reaching for the character-level tool when the question is about structure.

Frequently Asked Questions

Can I just use diff on two JSON files?

You can, and for tiny files formatted identically it's fine. It breaks down fast: JSON has no canonical line structure, so one reformatted document, reordered keys, or a minified pretty-printed pair makes a line diff report the entire file as changed. Parse both documents first and compare structure instead.

How does jq help compare JSON?

jq normalizes rather than diffs: sort keys with the -S flag and pretty-print, then pipe into diff. The one-liner: diff <(jq -S . a.json) <(jq -S . b.json). That kills key-order and formatting noise for free, but it still treats a reordered array as delete-plus-add pairs, because line diff has no concept of an item moving.

Why do two identical-looking JSON files show as different?

Almost always one of three invisibles: different key order (irrelevant per the JSON spec, but visible to text tools), different number formatting (1.0 versus 1), or a type difference like "7" the string versus 7 the number. A structural diff reports only the third case and ignores the first two.

What is deep equality in JSON?

Two values are deeply equal when they're the same type and equal all the way down: objects with the same keys (any order) pointing to deeply equal values, arrays with the same items in the same order, and identical primitives. It's the standard a structural diff uses to pair array items before deciding anything moved.

How do I see what changed in a JSON array?

Use a diff with array-move detection: items are paired by deep equality, unchanged items whose index shifted report as moves with from and to positions, items only in the old file as removals, and items only in the new file as additions. A changed item appears as a remove-add pair, since it's no longer equal to its old self.

Related Tools