Published · 7 min read
You have two versions of an API response and you need to know what changed. You paste them into a diff tool and get four hundred changed lines out of four hundred and twelve. Nothing meaningful changed at all — the serialiser just emitted the keys in a different order.
This is the difference between comparing text and comparing data, and it decides whether a diff is useful or noise.
What a text diff actually does
A text diff treats each file as a sequence of lines and finds the longest common subsequence between them — the largest set of lines appearing in both, in the same order. Lines outside it are marked added or removed. This is what git, patch, and every code review tool do, and for source code it is close to ideal: code is line-oriented, and a changed line is a meaningful unit.
The algorithm has no idea what the text means. A line is a string, and two strings are either equal or not. That indifference is what makes it work on every file type, and it is also exactly why it fails on structured data.
Three ways it fails on JSON
Key order
The JSON specification says object members are unordered. Two documents with the same keys in different order are the same document. A text diff sees completely different lines.
{ {
"name": "Ada", "active": true,
"team": "Platform", "team": "Platform",
"active": true "name": "Ada"
} }This is not hypothetical. Go maps iterate in randomised order by design, Python dictionaries preserved insertion order only from 3.7, and many serialisers offer key sorting as an option that half a team enables and half does not.
Formatting
Minified against pretty-printed is a total diff — one line against four hundred — with no data difference whatsoever. Two-space against four-space indentation changes every line. A trailing newline changes the last line.
Punctuation cascades
Add one field to the end of an object and the previously-last line gains a comma. The text diff reports two changed lines for one added field. Multiply that across a nested document and a handful of additions produce a diff twice the size it should be.
What a structural diff does instead
A structural diff parses both documents first and compares the resulting trees. Whitespace and key order have already been discarded by the parser — they were never part of the data — so they cannot produce false positives.
The output is expressed in terms of the data rather than the text: this key was added with this value, this key was removed, this key changed from this to that. It is reported at the path where the change occurred, so a change six levels deep is reported at that path rather than as a changed line in the middle of a wall of context.
~ user.plan "free" → "pro"
~ user.seats 3 → 25
- user.trialEndsAt "2026-01-01"
+ user.billingEmail "ops@example.com"Where structural diffing gets hard: arrays
Objects are easy, because keys give you an unambiguous correspondence between the two sides. Arrays have no such thing, and every structural diff tool has to choose a strategy with real trade-offs.
- By index — compare element 0 to element 0, and so on. Predictable and fast. Correct for positional arrays such as coordinates. Catastrophic when an element is inserted at the front, because every subsequent index shifts and the whole array reads as changed.
- By identity key — match elements by an id field. Correct for arrays of records and immune to reordering, but only possible when a stable key exists and the tool knows which field it is.
- By similarity — pair up the elements that resemble each other most. Handles insertion and reordering, but is expensive and occasionally pairs two records a human would not have paired.
Most tools, including this one, compare by index because it is predictable and never surprises you with a wrong pairing. The practical workaround when it bites: sort both sides by a stable key before comparing, and the diff collapses to the real changes.
When the text diff is the right tool
Structural diffing discards information, and sometimes that information is the point.
- When the bytes matter. A signed payload, a checksummed artifact, or anything where reformatting breaks a signature — the whitespace is part of the data.
- When you are reviewing the file as a file. A lockfile in a pull request is reviewed as text because that is what gets committed.
- When it will not parse. A structural diff needs two valid documents. If one is broken, a text diff is how you find out where.
- When comments matter. Parsing YAML discards comments, so a structural YAML diff cannot show you that someone deleted the explanation above a setting.
The same distinction elsewhere
Once you see the pattern it turns up everywhere. Spreadsheets should be compared cell by cell, not as exported text, because a column reordering is not a data change. XML should be compared as a tree, because attribute order is not meaningful. Images should be compared pixel by pixel, because two files with different bytes can be visually identical after re-encoding.
In every case the question is the same: what in this file is data, and what is merely how the data was written down? Compare the first and ignore the second, and the diff tells you something. Compare both and you get noise.
Frequently asked questions
- Can I make git do a structural diff?
- Yes, with a custom diff driver. Set a gitattributes entry mapping a file pattern to a diff tool, and git will use it for those files. It is worth configuring for generated JSON or XML artifacts that are otherwise unreviewable.
- Should I just sort keys before committing JSON?
- For files that live in version control and are read by humans, yes — canonical formatting makes text diffs meaningful and removes an entire class of spurious change. Do not do it to a payload whose bytes are signed or hashed.
- Why does my structural diff report a change when the values look the same?
- Almost always a type change: the number 3 became the string "3", or null became "". These look identical when printed and are genuinely different values, and the diff is right to flag them — that mismatch is a common cause of a client that works in one environment and breaks in another.