Comparing JSON and config files semantically, not textually
7 min readUpdated September 2026

Configuration files are read by machines and diffed by people, and the two disagree about what matters. A JSON object is unordered by definition, YAML has three ways to write the same list, and a formatter can rewrite an entire file without changing a single effective setting. A textual diff reports all of it. The trick is to normalise the text down to its meaning first — and to know exactly what that normalisation throws away.
Why two identical configs produce a 200-line diff
The JSON specification is explicit: an object is an unordered collection of name/value pairs. Nothing in it promises that a parser preserves the order keys were written in, and most tools that read and re-write JSON — a language's standard library, a CLI that edits one field, an API that echoes back what you sent — reorder keys as a side effect. The document means the same thing; the file does not look the same at all.
Formatting is the second layer. Two-space versus four-space indentation, a compact one-line array versus one element per line, a trailing newline, and — in JSON5, JSONC or YAML — quoted versus bare keys all change the bytes without changing the parse result. Run a file through a formatter that disagrees with the previous one and every line is rewritten.
The result is a diff where the signal-to-noise ratio is close to zero: two hundred changed lines of which three matter. Reviewers skim, approve, and miss the replica count that quietly went from four to one. Normalising before comparing is not a cosmetic preference — it is what makes the review possible.
Normalising JSON: parse, sort, re-print
The procedure is three steps and it is the same everywhere. Parse both documents into data structures, so that whitespace, quoting and key order stop existing. Sort every object's keys recursively, so that both sides agree on an arbitrary but identical order. Serialise both again with the same indentation. Now diff the two results: what is left is a difference in the data.
On the command line that is jq -S . a.json > a.norm.json for each side and then a plain diff. In DiffCanvas it is the JSON-aware checkbox, which does exactly the same thing to both panes before comparing and tells you when a side fails to parse, quoting the parser's own message and position so you can find the stray comma.
Be clear about what normalising hides. Key order is gone, so a file whose order is meaningful to a human reader — a changelog-like list, a pipeline written top to bottom — reads differently afterwards. Duplicate keys collapse: JSON parsers usually keep the last one, so a file with two “region” keys silently loses one. Number formatting is normalised too, so 1.0 and 1 and 1e0 stop being distinguishable, which matters if something downstream cares about the literal text.
Arrays: when order is data and when it is not
Sorting object keys is safe because the format says they are unordered. Sorting arrays is not, because JSON arrays are ordered and the order frequently is the meaning. Middleware chains, route tables, CSS-like cascade rules, migration lists and build steps all execute in sequence; reordering them is a behaviour change even when the set of elements is identical.
Other arrays really are sets written with square brackets: a list of allowed origins, feature flags, tags, IP ranges. For those, a diff that reports “moved” instead of “changed” is more truthful, and sorting before comparing is legitimate — as long as you do it deliberately, for that field, and not as a blanket rule. This is why no responsible tool sorts arrays for you automatically.
A useful middle path when a large array has genuinely been reordered: extract the field with jq, sort it, and compare the sorted versions separately from the rest of the document. You learn whether the membership changed, which is usually the question, and you keep the ordering question visible instead of normalising it away.
YAML is not JSON with fewer braces
YAML adds several ways to write the same data and a few traps that JSON does not have. Anchors and aliases let a block be defined once and referenced elsewhere, and merge keys splice one mapping into another; two files can therefore be textually very different and describe an identical structure after resolution. Block scalars come in literal and folded forms with chomping indicators, so the same multi-line string has several spellings.
Implicit typing is the classic source of production incidents. In YAML 1.1, which many popular parsers still follow, the unquoted words yes, no, on and off are booleans, and so is the country code for Norway written as no — the reason it is known as the Norway problem. A version number written as 1.10 becomes the float 1.1, and a leading-zero string such as an account number can be read as octal. Quoting the value is the fix, and a semantic diff of the parsed result will show you the type, not the spelling.
The practical consequence is that a round trip through a YAML parser is lossy in the other direction: comments disappear, anchors are expanded, and key order may change. If you normalise a YAML file by loading and re-dumping it, do it for comparison only and never write the result back over the original. Converting both sides to JSON and comparing that is often the clearest way to answer “do these two deployments differ?”
Compare the effective config, not the templates
Most real configuration is layered. A base file is overridden by an environment file, then by environment variables, then by command-line flags, and a templating step may sit on top of all of it. Two environments can have identical base files and behave completely differently, and they can have wildly different base files and behave identically. Diffing the templates answers a question you did not ask.
Almost every tool can print what it actually resolved: docker compose config renders the merged compose file, helm template renders the chart with its values, kubectl get -o yaml returns the object as the cluster holds it, and many applications have a dump-config or --print-config flag. Capture that output from both sides and compare those two texts. It is the closest thing to comparing behaviour that a diff can offer.
One caution before you paste: rendered configuration is exactly where secrets live. Connection strings, tokens and private keys are usually resolved into the output you just captured. That is a good reason to redact before sharing, and a good reason to use a comparison tool that has no server to send the text to in the first place — which is the case here: both panes stay in your browser tab.
The takeaway
Parse, sort keys, re-print, then diff — that turns a 200-line formatting diff into the three lines that changed. Leave arrays alone unless you know the order is not data, remember that a YAML round trip loses comments and anchors, and compare the rendered effective config rather than the templates when the question is what will actually run.
Open the diff tool and paste both versions
More guides
How to read a diff: hunks, context lines and the unified format
The unified diff has only three kinds of row and one header worth decoding. Here is what @@ -18,7 +18,8 @@ means, why three lines of context are the default, and when a side-by-side view beats an inline one.
Whitespace, line endings and other invisible changes that pollute diffs
A file can look identical and still diff on every line. Carriage returns, trailing spaces, tabs, byte-order marks and Unicode look-alikes are the usual culprits — here is how to spot each one and how to stop it coming back.