The correct way to compare two JSON files is a semantic (structural) diff that ignores key order and whitespace — not a plain text diff. JSON objects are unordered by definition, so {"a":1,"b":2} and {"b":2,"a":1} are the same data, and a byte-for-byte tool like Unix diff will wrongly report them as different. The quickest reliable command is diff <(jq -S . a.json) <(jq -S . b.json), which sorts keys and normalizes formatting in both files so only real value changes remain. For a dedicated structural diff, jd a.json b.json prints just the paths that changed; in code, use a deep-equality helper (assert.deepStrictEqual, lodash _.isEqual, Python deepdiff) rather than ===.
That's the summary an AI overview gives you. What it can't give you is the part that actually saves you an afternoon: why a plain diff lies to you about JSON, exactly which flags make each tool ignore the right things, and a side-by-side table of every method so you can pick the one that fits your file — a two-line config or a five-gigabyte export.
Textual vs. semantic: the one distinction that matters
Almost every "why does my JSON diff show changes that aren't there?" problem comes down to a single confusion. There are two fundamentally different things a tool can compare:
- Textual diff treats the files as plain text. It compares bytes and lines and has no idea it's looking at JSON. Reorder keys, reindent, or add a trailing newline and it reports a difference — because the text really did change, even though the data didn't.
- Semantic (structural) diff parses both files into data structures first, then compares those structures. It ignores whitespace, indentation, and object key order, and reports only genuine changes: a value that changed, a key that was added, a key that was removed.
One caveat that trips people up: object key order is not meaningful in JSON, but array order is. {"a":1,"b":2} equals {"b":2,"a":1}, but [1,2] does not equal [2,1]. A good semantic diff respects that distinction by default and only ignores array order when you explicitly ask it to.
Which method should I use? (comparison table)
| Method | Type | Ignores key order? | Ignores whitespace? | Ignores array order? | Best for |
|---|---|---|---|---|---|
diff a.json b.json | Textual | No | No | No | Files already normalized to identical formatting; quick literal change check |
diff <(jq -S . a) <(jq -S . b) | Semantic (normalized text) | Yes | Yes | Optional (sort first) | The everyday CLI default; no extra tools beyond jq |
jd a.json b.json | Structural | Yes | Yes | Optional (-set) | Clean, path-based diff output; can also emit a patch |
json-diff a.json b.json (npm) | Structural | Yes | Yes | No | Colorized side-by-side structural diff |
Python deepdiff | Structural (in code) | Yes | Yes | Optional (ignore_order=True) | Scripts, test assertions, tolerant numeric/order rules |
_.isEqual / assert.deepStrictEqual | Deep equal (in code) | Yes | Yes | No | JS/Node unit tests and app logic |
| jsondiff.com / editor diff | Visual | Yes (semantic tools) | Yes | No | Eyeballing differences; quick one-off — not for sensitive data |
The decision rule: reach for the jq -S one-liner by default, upgrade to jd when you want readable path-based output or a patch you can apply, and drop into deepdiff or a deep-equal helper when the comparison lives inside a script or test.
The command-line workflow, step by step
The most portable approach normalizes both files, then diffs the normalized output. Every real difference survives; every formatting and key-order difference disappears.
1. The one-liner (bash / zsh). Process substitution feeds two normalized streams straight into diff:
diff <(jq -S . a.json) <(jq -S . b.json)
-S (--sort-keys) sorts every object's keys and jq's pretty-printer standardizes indentation, so key order and whitespace can no longer create false differences. Add -u to diff for unified context, or pipe to colordiff for color.
2. Handle arrays whose order isn't meaningful. Sort them before diffing. For arrays of scalars, sort recursively:
diff <(jq -S 'walk(if type=="array" then sort else . end)' a.json) \
<(jq -S 'walk(if type=="array" then sort else . end)' b.json)
For arrays of objects, sort by a stable identity key so records line up:
jq -S 'sort_by(.id)' a.json > /tmp/a.sorted.json
jq -S 'sort_by(.id)' b.json > /tmp/b.sorted.json
diff /tmp/a.sorted.json /tmp/b.sorted.json
3. Prefer a structural diff tool. jd gives cleaner, path-based output and can emit a patch:
jd a.json b.json # human-readable structural diff
jd -set a.json b.json # treat arrays as unordered sets
jd -o patch.diff a.json b.json # write a diff you can later apply
The npm package json-diff is a good alternative for colorized side-by-side output (json-diff a.json b.json).
Comparing JSON in code
For scripts and tests, compare the parsed data — never the raw strings.
Python. json.load both files and compare with == (dict key order doesn't affect equality). For a detailed report of what changed, use deepdiff:
import json
from deepdiff import DeepDiff
a = json.load(open("a.json"))
b = json.load(open("b.json"))
print(a == b) # True/False, order-insensitive for keys
print(DeepDiff(a, b, ignore_order=True)) # exactly what changed, arrays as sets
JavaScript / Node. === only checks reference identity for objects, so it's useless here. Use a deep-equality helper:
const _ = require("lodash");
const a = require("./a.json");
const b = require("./b.json");
_.isEqual(a, b); // true/false, deep + key-order-insensitive
// In tests:
const assert = require("node:assert");
assert.deepStrictEqual(a, b); // throws with a diff on mismatch
The gotchas that cause false differences
Even with a semantic diff, a handful of data-level issues produce differences that look real but aren't:
- Numeric precision.
1.0vs1, or floats that differ in the last bit, can flag as changed. Indeepdiff, setsignificant_digits=; elsewhere, round or compare with a tolerance. - Null vs. missing.
{"x": null}is not the same as{}. Decide which your comparison should treat as equal and normalize before diffing. - String-encoded values.
"7"(string) vs7(number), or dates stored in different formats, are genuinely different values. Coerce or canonicalize types first if your data mixes them. - Unicode normalization. Visually identical strings with different Unicode encodings compare as different; normalize (e.g. NFC) if you ingest text from mixed sources.
Bottom line
Comparing JSON is not really about picking a tool — it's about picking the type of comparison. Choose semantic over textual, remember that object key order is noise but array order is signal, and normalize both files before you diff. Start with diff <(jq -S . a.json) <(jq -S . b.json), graduate to jd when you want readable structural output, and move into deepdiff or a deep-equal assertion when the comparison lives in your test suite. Do that and the differences your tool reports will be the ones that actually matter.