"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.
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.
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.
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.
| Change | jq + diff shows | Structural diff shows |
|---|---|---|
| Key reordered | Nothing (after -S) | Nothing |
| Reformatted | Nothing (after jq) | Nothing |
| Item moved in array | Delete + add pair | Moved 2→0 |
| Value changed | Changed line | Modified entry, old → new |
| Duplicate values reshuffled | Noise | Moves, 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.
Paste two documents; get additions, removals, modifications, and array moves as JSON paths, with an ignore-array-order toggle. Nothing is uploaded.
JSON Diff →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.
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.
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.
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.
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.
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.
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.