How to Compare JSON Files Without a Library
Walk both structures yourself when the dependency would be overkill, and use the browser tool when reviewing the diff visually is faster.
Last updated
Bringing in DeepDiff or jsondiffpatch for a single comparison feels heavy. The standard library can give you a path-level diff in about 30 lines, and that is often all you need.
1. JavaScript: walk both objects recursively
function diff(a, b, path = '') {
const out = [];
const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]);
for (const key of keys) {
const next = path ? `${path}.${key}` : key;
if (!(key in a)) out.push({ path: next, type: 'added', value: b[key] });
else if (!(key in b)) out.push({ path: next, type: 'removed', value: a[key] });
else if (typeof a[key] === 'object' && a[key] !== null &&
typeof b[key] === 'object' && b[key] !== null) {
out.push(...diff(a[key], b[key], next));
} else if (a[key] !== b[key]) {
out.push({ path: next, type: 'modified', from: a[key], to: b[key] });
}
}
return out;
}This produces a flat list of {path, type, value} entries, which is easy to log, snapshot, or feed into a test assertion.
2. Python: same shape with json and recursion
def diff(a, b, path=''):
out = []
keys = set((a or {}).keys()) | set((b or {}).keys())
for key in keys:
nxt = f'{path}.{key}' if path else key
if key not in a:
out.append((nxt, 'added', b[key]))
elif key not in b:
out.append((nxt, 'removed', a[key]))
elif isinstance(a[key], dict) and isinstance(b[key], dict):
out.extend(diff(a[key], b[key], nxt))
elif a[key] != b[key]:
out.append((nxt, 'modified', a[key], b[key]))
return out3. Handle arrays the way your data needs
The recursive walks above only descend into dicts and objects. Arrays are compared with !==. If you need positional array diffs, replace the array branch with an indexOf walk; if you need set-style array comparison, sort both sides before comparing. There is no one-size-fits-all answer because arrays mean different things in different schemas.
4. Five differences that are not really differences
Most of the noise in a hand-rolled JSON diff comes from comparing the file as text when you meant to compare the data. These five account for nearly all of it.
| Looks like a change | Why it is not | How to neutralise it |
|---|---|---|
| Reordered keys | JSON objects are unordered by specification, so {"a":1,"b":2} and {"b":2,"a":1} are the same value. | Parse before comparing, or serialise with sorted keys (json.dumps(obj, sort_keys=True)). |
| 1 versus 1.0 | Both parse to the same number in JavaScript, and compare equal in Python, but they serialise to different text. | Compare parsed values, not re-serialised strings. |
| Unicode escapes | "\u00e9" and "é" parse to an identical string; only the encoding of the source file differs. | Parse both sides. In Python, ensure_ascii=False keeps output consistent. |
| Float precision | 0.1 + 0.2 is 0.30000000000000004, so a recomputed value differs in the last digits. | Round to a fixed precision, or compare numbers with a tolerance instead of equality. |
| Indentation and trailing newline | Whitespace outside strings carries no meaning in JSON. | Never diff formatted JSON as text — parse it, or normalise formatting on both sides first. |
The single rule underneath all five: parse, then compare. A text diff answers "did the file change", which is a different and usually less useful question than "did the data change".
5. Decide what an array means before you diff it
The recursive walks above descend into objects but compare arrays with a plain inequality, which is deliberate — there is no correct default. An array can mean three different things, and each wants a different comparison.
If it is an ordered sequence, such as pipeline stages or a changelog, compare by index and report position changes as real differences. If it is an unordered set, such as tags or permissions, sort both sides on a stable key first, or compare as sets, so reordering is not reported. If it is a keyed collection — the most common case in API payloads, where each element has an id — index both arrays by that field and compare the resulting maps. That last option is the only one that reports "item 42 changed" rather than "the whole array changed", which is almost always what you actually want to read.
Getting this wrong is the single largest source of unusable JSON diffs. A reordered 200-element array compared positionally reports 200 modifications and tells you nothing.
When to switch to the browser tool
If you only have two JSON files and a human is going to read the diff, writing this is overkill. Open JSON Diff, paste both sides, and you have a side-by-side view in seconds. For API responses, use API Response Diff; for package files, package.json Diff.
For the broader topic, browse the JSON Comparison Tools hub.
Frequently asked questions
Can I compare two JSON files without installing anything?
Yes. Both the JavaScript and Python recipes above use only the standard library and run in about 30 lines. For a one-off comparison a human will read, JSON Diff in the browser is faster still and needs no code at all.
Why does my diff report changes when the files look the same?
Almost always because you are comparing text rather than parsed data. Reordered keys, 1 versus 1.0, unicode escapes, float precision, and indentation all change the bytes without changing the value. Parse both sides first.
Is JSON.stringify(a) === JSON.stringify(b) a valid equality check?
Only if you control how both sides were produced. Key order is preserved by JSON.stringify, so two objects with identical contents in a different insertion order produce different strings. It is also undefined for NaN, Infinity, and undefined values.
How should I handle arrays of objects?
Index both arrays by a stable identity field and compare the resulting maps. Comparing positionally reports the entire array as changed as soon as one element is inserted or reordered.
When is a library actually worth it?
When the diff is consumed by code rather than a person — snapshot tests, change detection in a pipeline, or anything needing configurable tolerance and ignore rules. DeepDiff and jsondiffpatch handle those cases well. For eyeballing two files, they are overhead.