Text Diff vs Semantic Diff
The instinct is to run diff a.json b.json or git diffand be done. That works for eyeballing formatting changes but gives noisy, misleading output for semantic comparison. Consider:
# a.json
{"name": "Alice", "age": 30}
# b.json
{"age": 30, "name": "Alice"}These documents are semantically identical — JSON objects have no key ordering. But diff shows two lines changed. Conversely:
# a.json
{"user": {"role": "admin"}}
# b.json — one deeply nested change
{"user": {"role": "user"}}diffshows this as "the whole line changed" — losing the information about which field. A semantic diff produces:
{
"user": {
"role": ["admin", "user"] ← value changed from "admin" to "user"
}
}Or as an RFC 6902 patch:
[
{ "op": "replace", "path": "/user/role", "value": "user" }
]JSON Diff in Node.js — jsondiffpatch
// npm install jsondiffpatch
const jsondiffpatch = require('jsondiffpatch');
const a = { name: 'Alice', age: 30, tags: ['admin', 'user'] };
const b = { name: 'Alice', age: 31, tags: ['admin', 'guest'] };
const delta = jsondiffpatch.diff(a, b);
console.log(JSON.stringify(delta, null, 2));
// {
// "age": [30, 31], ← value changed
// "tags": {
// "_1": ["user", 0, 0], ← element at index 1 removed
// "1": ["guest"] ← "guest" inserted at index 1
// }
// }
// Apply the delta to reproduce b from a
jsondiffpatch.patch(a, delta);
// a is now equal to b
// Reverse the delta to undo
const inverse = jsondiffpatch.reverse(delta);
jsondiffpatch.patch(a, inverse);
// a is back to originalFor a human-friendly rendered diff, use the jsondiffpatch/formatters/consolesubmodule which outputs coloured terminal text, or formatters/html which produces a side-by-side HTML view you can drop into a page.
JSON Diff in Node.js — RFC 6902 Patch (fast-json-patch)
// npm install fast-json-patch
const jsonpatch = require('fast-json-patch');
const a = { user: { name: 'Alice', role: 'admin' }, count: 5 };
const b = { user: { name: 'Alice', role: 'user' }, count: 6, active: true };
// Generate patch
const patch = jsonpatch.compare(a, b);
console.log(patch);
// [
// { op: 'replace', path: '/user/role', value: 'user' },
// { op: 'replace', path: '/count', value: 6 },
// { op: 'add', path: '/active', value: true }
// ]
// Apply patch
const result = jsonpatch.applyPatch(structuredClone(a), patch).newDocument;
// result equals bRFC 6902 is the interchange format you want between systems. Use it in PATCH /api/resource HTTP endpoints, event streams, and any state-synchronisation protocol.
JSON Diff in Python — deepdiff
# pip install deepdiff
from deepdiff import DeepDiff
a = {'name': 'Alice', 'age': 30, 'tags': ['admin', 'user']}
b = {'name': 'Alice', 'age': 31, 'tags': ['admin', 'guest']}
diff = DeepDiff(a, b)
print(diff.to_json(indent=2))
# {
# "values_changed": {
# "root['age']": {"new_value": 31, "old_value": 30},
# "root['tags'][1]": {"new_value": "guest", "old_value": "user"}
# }
# }
# Ignore key order (default), ignore type differences, tolerate float precision
diff = DeepDiff(a, b, ignore_order=True, ignore_type_in_groups=[(int, float)], significant_digits=3)JSON Diff in Python — jsonpatch (RFC 6902)
# pip install jsonpatch
import jsonpatch, json
a = {'user': {'role': 'admin'}, 'count': 5}
b = {'user': {'role': 'user'}, 'count': 6, 'active': True}
patch = jsonpatch.make_patch(a, b)
print(patch.to_string())
# [{"op": "replace", "path": "/user/role", "value": "user"},
# {"op": "replace", "path": "/count", "value": 6},
# {"op": "add", "path": "/active", "value": true}]
# Apply
result = patch.apply(a)
assert result == bCommand-Line JSON Diff Tools
jd — dedicated JSON diff (Go)
brew install jd
# Simple diff
jd a.json b.json
# Emit RFC 6902 patch
jd -f patch a.json b.json
# Apply a patch to reconstruct b
jd -p patch.json a.json > b.json
# Ignore array order (treat as set)
jd -set a.json b.jsonjson-diff — Node package
npm install -g json-diff
# Coloured terminal output
json-diff a.json b.json
# Full paths only, machine-parseable
json-diff -k a.json b.jsonjq — approximate with -S flag
# Canonicalise both files (sort keys), then diff as text
diff <(jq -S . a.json) <(jq -S . b.json)
# Boolean equality check
jq -n --argfile a a.json --argfile b b.json '$a == $b'
# Which keys are in a but not b?
jq -n --argfile a a.json --argfile b b.json '
($a | paths) as $ap
| ($b | paths) as $bp
| $ap - $bp'Common JSON Diff Gotchas
- Array element identity. Is
[1, 2, 3]→[3, 2, 1]a change? Text diff says yes.deepdiffwithignore_order=Truesays no. Pick the semantics you want and configure the differ explicitly. - Numeric precision.
1.0vs1— same number, different JSON tokens. Most parsers coerce both to Pythonfloator JS Number, so semantic diff treats them as equal. Text diff does not. - Large arrays.
jsondiffpatchuses a longest-common-subsequence algorithm for arrays, which is O(n·m). For 100k-element arrays this crawls. Either treat arrays as sets or use a schema-aware differ that knows the "id" field. - Whitespace inside strings. A semantic differ correctly reports
"hello"vs" hello"as changed — but if the whitespace was accidentally introduced by an editor, you may want to normalise strings before diffing. - Deleting vs setting null.
{"x": null}and{}are different documents (one has key "x", one doesn't). RFC 6902 distinguishes{op: "remove"}from{op: "replace", value: null}— pick the correct operation.
Diff in a CI Pipeline
A common use case: your test suite has a "golden file" JSON output, and each CI run regenerates it. If the output changes unexpectedly, fail the build.
#!/bin/bash
# ci-check-golden.sh
generated=$(mktemp)
./my-tool > "$generated"
if ! jd -f patch tests/golden.json "$generated" > /dev/null 2>&1; then
echo "❌ Golden file differs. Diff:"
jd tests/golden.json "$generated"
echo ""
echo "If the change is intentional, update the golden with:"
echo " cp $generated tests/golden.json"
exit 1
fi
echo "✅ Output matches golden"Key Facts
- Standard patch format:
- RFC 6902 JSON Patch
- Node library:
- jsondiffpatch (human), fast-json-patch (RFC 6902)
- Python library:
- deepdiff (human), jsonpatch (RFC 6902)
- CLI:
- jd (Go, most feature-complete), json-diff (Node)
- Order sensitivity:
- Arrays are ordered by default; keys are not
- Text diff alternative:
- diff on jq -S canonicalised output
Related JSON Tools
- Format JSON Online — canonicalise both inputs before diffing
- Sort JSON Keys — required for text-based diff
- Validate JSON Online — check syntax first
- Fix Invalid JSON — repair before diff
- JSON Formatter Python — Python JSON handling guide
- JSON Formatter JavaScript — Node JSON handling guide