Comparing and merging JSON documents
Why text diffs fail on JSON, how structural comparison and key sorting help, and how merge strategies treat objects and arrays.
Two JSON documents that mean the same thing can look completely different as text: keys in another order, different indentation, one minified and one pretty-printed. A plain text diff drowns in that noise. And when you need to combine documents, such as a default configuration plus overrides, a naive merge loses nested settings. This guide covers structural comparison and merging, and the choices that decide the result.
Why text diffs fail on JSON
These two documents are equal as data:
{"name":"Ada","roles":["admin","dev"]}
{
"roles": ["admin", "dev"],
"name": "Ada"
}A line-based diff reports almost every line as changed. JSON objects are unordered, and whitespace between tokens has no meaning, so neither should count as a difference. There are two ways to fix this: normalize both documents and then diff them as text, or compare them structurally.
Option 1: normalize, then diff
Sort JSON keys orders every object’s keys alphabetically at every level, while leaving arrays in their original order. Two documents with the same content then produce identical text, and a normal diff shows only real changes. This is also a good habit for configuration and translation files kept in version control, because sorted files produce smaller, clearer diffs and fewer merge conflicts.
Option 2: compare structurally
Compare JSON walks both documents and reports each difference by path. Comparing {"name":"Ada","roles":["admin","dev"],"active":true,"age":36} with {"age":"36","name":"Ada","roles":["dev","admin"],"email":"[email protected]"} gives five changes:
/roles/0changed from "admin" to "dev", and/roles/1from "dev" to "admin". Array order counts./activewas removed./agechanged from the number 36 to the string "36". A text diff would hardly notice, but strict clients will./emailwas added.
The key order changed completely, but that is not reported. Paths use JSON Pointer syntax (RFC 6901), the same notation as JSON Patch and JSON Schema references, so you can jump to any change in the JSON tree viewer.
Whether array order “counts” depends on the data. For a list of steps it matters. For a set of tags it may not. Structural comparison treats arrays as ordered, which is the safe default. If order does not matter for your data, sort the arrays before comparing.
Merging documents
Merging answers a different question: given a base document and an incoming one, what is the combined result? With merge JSON, the incoming document always wins a conflict. The strategy decides what counts as a conflict:
Base: {"server":{"port":8080,"host":"localhost"},"plugins":["auth"]}
Incoming: {"server":{"port":9090},"plugins":["cache"],"debug":true}- Deep merge, arrays replaced: server becomes
{"port":9090,"host":"localhost"}, plugins becomes["cache"], and debug is added. This is how most configuration systems layer defaults and overrides. - Deep merge, arrays concatenated: the same, but plugins becomes
["auth","cache"]. Duplicates are kept. - Shallow merge: server is replaced whole, so
hostdisappears. This matches JavaScript’s{ ...base, ...incoming }.
Arrays are the hard part, because there is no universally right answer. Lists of allowed origins or plugins usually need concatenation. Coordinates, ordered steps, or anything where duplicates cause trouble usually need replacement. Check the conflict preview before exporting.
One more subtlety: in this merge, null is an ordinary value. It replaces the base value rather than deleting the key. That differs from JSON Merge Patch (RFC 7396), where null means “remove this key”. If you are implementing Merge Patch semantics in an API, do not assume a general-purpose merge behaves the same way.
A workflow for reviewing changes
- Validate both documents with the JSON validator. A syntax error makes any comparison meaningless.
- Run a structural comparison to list real changes.
- Check type changes and removed keys first. These are the changes that break clients.
- If you are combining documents, merge with an explicit array policy and review the conflicts.
- Sort keys in the final document before committing it, so future diffs stay small.
For very large documents, flattening into path records lets you compare line by line in any diff tool, or load the records into a spreadsheet, and unflatten rebuilds the exact structure afterwards.
