What Changed Between These Two Documents
The diff between the original and modified objects shows five changes:
agechanged from30to31emailchanged fromalice@example.comtoalice@newdomain.comrolesgained a third element:"viewer"settings.themechanged from"dark"to"light"settings.languagewas added with value"en"
name, settings.notifications, and the overall structure stayed the same.
Textual vs Semantic JSON Diff
A textual diff compares the two documents as strings, line by line. This works well when both documents were formatted consistently: same indentation, same key order. If key order differs between the two documents, a textual diff will mark every line as changed even though the data is identical.
A semantic diff parses both documents first, then compares the data structures. It does not care about key order, whitespace, or indentation. It reports changes in terms of paths and values: age changed from 30 to 31, not line 3 changed.
For comparing JSON documents from different sources (different APIs, different serializers), semantic diff is more reliable. For tracking changes to a JSON file in version control where formatting is consistent, textual diff (as shown here) works well and is immediately readable.
Preparing JSON for a Clean Textual Diff
Before diffing two JSON documents as text, format both with the same settings. Inconsistent indentation or key ordering will produce noisy diffs where every line appears changed.
Format with jq using consistent indentation:
jq --sort-keys . original.json > original_formatted.json
jq --sort-keys . modified.json > modified_formatted.json
diff original_formatted.json modified_formatted.json
The --sort-keys flag sorts object keys alphabetically. If both documents are sorted the same way, key order differences will not appear as spurious changes in the diff.
JSON Patch Format (RFC 6902)
The changes between these two documents expressed as a JSON Patch document:
[
{"op": "replace", "path": "/age", "value": 31},
{"op": "replace", "path": "/email", "value": "alice@newdomain.com"},
{"op": "add", "path": "/roles/2", "value": "viewer"},
{"op": "replace", "path": "/settings/theme", "value": "light"},
{"op": "add", "path": "/settings/language", "value": "en"}
]
JSON Patch is useful for API update endpoints where you want to send only the changed fields rather than the full document. The test operation lets you make changes conditional on a current value, enabling optimistic concurrency control.
JSON Merge Patch Format (RFC 7396)
The same changes expressed as a JSON Merge Patch:
{
"age": 31,
"email": "alice@newdomain.com",
"roles": ["admin", "editor", "viewer"],
"settings": {
"theme": "light",
"language": "en"
}
}
Merge Patch is simpler but less precise. It cannot express array element additions without resending the full array. It uses null to signal key deletion. For simple objects without array manipulation, Merge Patch is easier to construct and read.
CLI Tools for JSON Diff
jd
jd is a dedicated JSON diff tool that produces path-based semantic output:
jd original.json modified.json
Output:
@ ["age"]
- 30
+ 31
@ ["email"]
- "alice@example.com"
+ "alice@newdomain.com"
@ ["roles",2]
+ "viewer"
@ ["settings","language"]
+ "en"
@ ["settings","theme"]
- "dark"
+ "light"
Each @ line shows the path to the changed value. This format makes nested changes immediately clear.
jq with diff
A quick comparison using only standard tools:
diff <(jq --sort-keys . original.json) <(jq --sort-keys . modified.json)
This formats and sorts both files before running a standard textual diff, giving you clean line-by-line output without installing an additional tool.
Python deepdiff
For scripted JSON comparison in Python:
from deepdiff import DeepDiff
import json
with open("original.json") as f:
original = json.load(f)
with open("modified.json") as f:
modified = json.load(f)
diff = DeepDiff(original, modified)
print(diff)
deepdiff handles nested structures, type changes, and list item additions without requiring consistent key order or formatting.