JSON
Why key order and whitespace shouldn't show up in a JSON diff
Key order and whitespace shouldn't appear in a JSON diff because they aren't part of the data. RFC 8259 defines a JSON object as an unordered collection of name/value pairs and allows insignificant whitespace around structural characters. A text diff still reports them as changes. A structural (semantic) JSON diff parses both documents first, so it reports only real differences in values, types and structure.
What the JSON spec says about key order and whitespace
RFC 8259, the current JSON standard, settles both points:
- Key order: "An object is an unordered collection of zero or more name/value pairs." The objects
{"a":1,"b":2}and{"b":2,"a":1}hold the same data. - Whitespace: "Insignificant whitespace is allowed before or after any of the six structural characters" (
{ } [ ] : ,). That whitespace can be any mix of spaces, tabs, line feeds and carriage returns. Indentation, line breaks, CRLF vs LF and a trailing newline at the end of the file don't change what a JSON document means.
A structural JSON comparison parses each document into values (objects, arrays, strings, numbers, booleans, null) and compares those values. A text diff compares the characters or lines. Most of the confusion in JSON reviews comes from mixing up these two kinds of diff.
Example: a 14-line git diff with zero data changes
Here's a config file stored minified on one line, with no trailing newline:
{"name":"build-agent","id":4182,"enabled":true,"tags":["ci","linux"],"limits":{"memoryMb":8192,"cpu":4}}
Someone opens it in a tool that pretty-prints with two-space indentation, sorts keys alphabetically and adds a final newline. Then they commit it. Here's what git diff shows:
@@ -1 +1,13 @@
-{"name":"build-agent","id":4182,"enabled":true,"tags":["ci","linux"],"limits":{"memoryMb":8192,"cpu":4}}
\ No newline at end of file
+{
+ "enabled": true,
+ "id": 4182,
+ "limits": {
+ "cpu": 4,
+ "memoryMb": 8192
+ },
+ "name": "build-agent",
+ "tags": [
+ "ci",
+ "linux"
+ ]
+}
That's 13 insertions and 1 deletion, and not one value changed. A structural diff of the same two files finds zero changes. Every key and value, and every array position, is identical.
Example: one real change hidden in the noise
Now suppose the same commit also raised the memory limit from 8192 to 16384. git diff --stat reports exactly the same thing as before: 13 insertions(+), 1 deletion(-). The real change is on one of those 13 green lines, and nothing marks it as different from the formatting noise. A reviewer has to compare a 100-character line against 13 reformatted lines in their head.
A structural diff reports one entry:
changed $.limits.memoryMb 8192 -> 16384
This is the core argument for a semantic JSON diff. When formatting and data change in the same commit, a line diff can't separate them. JSON Mirror compares parsed structure, so this example produces exactly one "changed" entry with its path. (If the path notation is new to you, see Reading JSON paths in a diff.)
Where JSON formatting noise comes from
Serializers that reorder keys
Different tools write the same object in different key orders:
- Python's
json.dumps(obj, sort_keys=True)sorts keys. Go'sencoding/jsonsorts map keys when it marshals a map. - PostgreSQL's
jsonbtype doesn't preserve key order or whitespace, so data that goes through it comes back in a different order. - JavaScript objects list integer-like keys first, in ascending order.
JSON.stringify(JSON.parse('{"b":1,"10":2,"2":3}'))returns{"2":3,"10":2,"b":1}, even though nobody asked for a reorder.
Pretty-printed vs minified
APIs often return minified JSON, while files in a repository are usually indented. Indent width (2 spaces, 4 spaces, tabs) also differs between editors and formatters. Converting between any of these rewrites nearly every line.
Trailing newline and line endings
JSON.stringify doesn't add a final newline, but many editors and command-line tools do. Git flags the difference with "No newline at end of file", and CRLF vs LF conversion on Windows can touch every line. To a JSON parser, both are insignificant whitespace.
What a structural JSON comparison still treats as meaningful
Ignoring formatting doesn't mean ignoring everything. These differences are real, and a good structural diff reports them:
- Array order. Arrays are ordered in JSON, so
["ci","linux"]and["linux","ci"]are different. JSON Mirror compares arrays by index, so this shows up as two changed entries,$.tags[0]and$.tags[1]. It doesn't detect moves. If your arrays are really sets or lists of records with IDs, read Comparing JSON arrays: by position or by identity? - Types.
"1"(a string) and1(a number) are different values. Likewise"true"andtrue. JSON Mirror reports a type change as one change at that path. - Missing vs null.
{"a":null}and{}aren't the same document, and JSON Mirror treats them as different.
Number representation subtleties
A JavaScript-based diff parses every number as an IEEE-754 double. That has two consequences:
1.0and1parse to the same number, as do1e2and100, so they compare equal. Usually that's what you want.- Integers beyond 2^53 - 1 (9,007,199,254,740,991) lose precision.
JSON.parse("9007199254740993")returns9007199254740992, so a change between those two values is invisible. For large IDs or exact decimal amounts, compare with a parser that keeps integers exact (Python'sjsonmodule does) or store such values as strings.
Duplicate keys
RFC 8259 says names within an object SHOULD be unique, but it doesn't forbid duplicates. JSON.parse keeps the last value, so {"a":1,"a":2} parses to {"a":2} and compares equal to it. A text diff would show the duplicate. A structural diff built on JSON.parse won't, so if duplicates might matter in your data, check for them separately.
Text diff vs structural diff
| Difference between the two files | Text diff (e.g. git diff) | Structural diff |
|---|---|---|
| Indentation, spaces, line breaks | Changed lines | Ignored |
| Minified vs pretty-printed | Almost every line changed | Ignored |
| Object key order | Changed lines | Ignored |
| Trailing newline, CRLF vs LF | Flagged | Ignored |
| A changed value | A changed line, possibly buried in noise | One entry with its path |
| Array items reordered | Changed lines | Changed entries (when compared by index) |
"1" vs 1 | Changed line | Type change |
1.0 vs 1 | Changed line | Equal (same parsed number) |
| Integers beyond 2^53 - 1 | Changed line | May be missed by double-based parsers |
| Duplicate keys | Visible | Collapsed to the last value |
| Invalid JSON | Still diffs the text | Can't compare until it parses |
How to make git diff ignore JSON key order and whitespace
If you want to keep using git for review, normalize both versions before comparing. jq -S . re-indents the document and sorts object keys recursively:
diff <(jq -S . before.json) <(jq -S . after.json)
To make git diff do this automatically, add a textconv filter. In .gitattributes:
*.json diff=json
Then configure the filter:
git config diff.json.textconv "jq -S ."
Git now shows diffs of the normalized output, while the files in the repository stay unchanged. A few caveats apply. A file that isn't valid JSON makes jq fail, so the diff fails too. Array order is still compared line by line. And jq doesn't necessarily normalize how a number is written, so 1.0 vs 1 may still appear. Older jq versions (before 1.7) also convert numbers to doubles, which brings back the large-integer problem.
A short script works too. In Python, json.load(f1) == json.load(f2) ignores key order and whitespace and keeps large integers exact. One caveat: Python treats True == 1 and 1 == 1.0 as equal, so [true] and [1] would compare equal.
Try a structural JSON diff
A JSON diff should answer one question: what changed in the data? Key order, indentation and trailing newlines don't affect the data, so they shouldn't take up space in the result. Array order, types, null vs missing, and changed values do, so they should.
To see this on your own files, paste both versions into JSON Mirror. It ignores whitespace and key order, lists each change with its path, and runs entirely in your browser, so nothing is uploaded. For large integers or arrays where position isn't meaningful, use the alternatives above.
Frequently asked questions
Does key order matter in JSON?
No. RFC 8259 defines a JSON object as an unordered collection of name/value pairs, so {"a":1,"b":2} and {"b":2,"a":1} carry the same data. Software should not rely on member order, even though some parsers happen to preserve it.
How do I compare two JSON files and ignore whitespace and key order?
Parse both files and compare the resulting structures instead of the text. You can do this with a structural diff tool, with jq by running each file through jq -S . before diffing, or with a short script that loads both files and compares the parsed values.
Why does git diff show every line changed in my JSON file?
git diff compares lines of text, so re-indenting, minifying, sorting keys or changing line endings changes almost every line even when the data is identical. Normalizing both versions with jq -S . before diffing, or configuring a textconv filter, removes that noise.
Does a semantic JSON diff treat 1 and 1.0 as the same?
In tools that parse with JavaScript's JSON.parse, yes: 1 and 1.0 both become the same number, so they compare equal. The string "1" and the number 1 are still different types and are reported as a change.
Is array order ignored in a structural JSON comparison?
Usually not. JSON arrays are ordered, so most structural diffs, including JSON Mirror, compare arrays by position. Reordering items shows up as changes at the affected indexes.