How to Compare Two JSON Files and Find the Real Differences
Testing 8 min readUpdated October 12, 2026
Two JSON documents can contain exactly the same data and still look completely different in a text diff, because of whitespace, key order or formatting. Comparing JSON well means comparing what the data means, not how it happens to be laid out. This guide shows how to do that by hand, in code and with a visual tool.
Why a plain text diff misleads you
These two documents are equal as JSON, yet a line-by-line diff reports every line as different:
{"name":"Asha","age":30,"tags":["a","b"]}{
"tags": ["a", "b"],
"age": 30,
"name": "Asha"
}A JSON object is an unordered collection of key-value pairs, so key order is not a real difference. Whitespace is not either. What does matter is the order of items in an array, because arrays are ordered.
Step 1: normalize both files
The simplest reliable approach is to format both files the same way and sort their keys, then use any text diff tool. Sorting and formatting remove the differences that do not matter.
# Sort keys and use the same indentation, then compare
diff <(jq -S . a.json) <(jq -S . b.json)
# Or with Python only
diff <(python -c "import json,sys; print(json.dumps(json.load(open('a.json')), indent=2, sort_keys=True))") \
<(python -c "import json,sys; print(json.dumps(json.load(open('b.json')), indent=2, sort_keys=True))")This catches added, removed and changed values, and it works well in a code review or a CI job. Its weakness is that it reports differences by line, not by data path, and a change deep in a long file can be hard to locate.
Step 2: compare structurally
A structural comparison walks both documents together and reports each difference with its path, such as $.address.city. Here is a small, readable implementation in Python:
import json
def diff(a, b, path="quot;):
"""Return a list of (kind, path, detail) tuples describing how b differs from a."""
changes = []
if isinstance(a, dict) and isinstance(b, dict):
for key in sorted(set(a) | set(b)):
child = f"{path}.{key}"
if key not in a:
changes.append(("added", child, b[key]))
elif key not in b:
changes.append(("removed", child, a[key]))
else:
changes += diff(a[key], b[key], child)
elif isinstance(a, list) and isinstance(b, list):
for i in range(max(len(a), len(b))):
child = f"{path}[{i}]"
if i >= len(a):
changes.append(("added", child, b[i]))
elif i >= len(b):
changes.append(("removed", child, a[i]))
else:
changes += diff(a[i], b[i], child)
# In Python True == 1, so compare booleans explicitly
elif a != b or isinstance(a, bool) != isinstance(b, bool):
changes.append(("changed", path, (a, b)))
return changes
old = json.load(open("a.json", encoding="utf-8"))
new = json.load(open("b.json", encoding="utf-8"))
for kind, where, detail in diff(old, new):
print(kind.upper().ljust(8), where, detail)Given an old document with city "Pune" and a new one with "Mumbai", plus a new email key, it prints lines such as:
CHANGED $.address.city ('Pune', 'Mumbai')
ADDED $.email asha@example.comThe array problem
Arrays are where comparisons get ambiguous. If one item is inserted at the start of a list of 100 items, an index-by-index comparison will report that all 100 items changed, when in reality one item was added.
- The order matters (a list of steps, a time series): compare by position, as above.
- The order does not matter (a set of tags): sort both arrays first, or compare them as sets.
- Items have a stable identifier (users with an
id, products with asku): index each array by that key first, then compare item to item. A removed item then shows up as removed, and a reordered list shows no change.
def by_id(items, key="id"):
return {item[key]: item for item in items}
changes = diff(by_id(old["users"]), by_id(new["users"]), "$.users")Differences that are easy to miss
| Situation | Why it matters |
|---|---|
null vs a missing key | Often mean different things: "no value" vs "not provided". Decide whether your comparison treats them the same. |
1 vs 1.0 vs "1" | The first two are the same number in most parsers. The third is a string and a real difference. |
| Floating-point values | 0.1 + 0.2 is not exactly 0.3. Compare with a tolerance when numbers come from calculations. |
| Date and time strings | 2026-10-12T10:00:00Z and 2026-10-12T15:30:00+05:30 are the same instant but different strings. |
| Unicode escapes | "\u00e9" and "é" are the same string. Parsing before comparing handles this. |
| Very large integers | Parsers that use floating point may round them. Compare them as strings if exactness matters. |
A visual comparison tool is often faster
For one-off checks, a side-by-side viewer that highlights added, removed and changed values is quicker than any script. Load the original on the left and the new version on the right, and scan the highlights. TrueJson's Compare view does this and also works with documents that are formatted differently.
Typical uses are checking that an API response did not change after a release, reviewing configuration drift between environments, and verifying that a data migration preserved every field.
Frequently asked questions
Does key order matter when comparing JSON?
No. By definition a JSON object is an unordered set of pairs. Arrays are ordered, so the order of array items does matter unless you decide otherwise.
How do I ignore certain fields, such as timestamps?
Remove those keys from both documents before comparing (for example with jq 'del(.updatedAt)'), or skip those paths in your diff function.
How do I compare very large JSON files?
Normalize with jq -S and use a text diff, which streams well, or compare record by record using a stable identifier. Browser tools can slow down on files of many megabytes.
Is there a standard format for describing JSON differences?
Yes. JSON Patch (RFC 6902) describes changes as a list of operations such as add, remove and replace with a path to each value, and many libraries can generate and apply it.