How to Sort JSON Keys Alphabetically for Clean, Stable Diffs
Sort JSON keys alphabetically to kill noisy git diffs. Learn recursive key sorting, why deterministic order stabilizes snapshots, and a worked example.
How to Sort JSON Keys Alphabetically for Clean, Stable Diffs
Two services write the same config object. One emits {"port":80,"host":"x"}, the other {"host":"x","port":80}. Nothing changed, yet git diff paints a wall of red and green. The fix is boring and effective: pick one canonical key order, alphabetical, and stick to it everywhere. Once both files agree on order, the diff only moves when a value actually moves.
This post walks through why alphabetical key order matters, how recursive sorting handles deeply nested config, and where deterministic JSON pays off in snapshots, caching, and code review. I'll keep the examples concrete and small enough to type out by hand.
Why key order causes phantom diffs
JSON objects have no canonical key order in the spec. An object is a set of name/value pairs, and serializers are free to emit them however they like, insertion order, hash order, or whatever a library decided last release. That freedom is invisible until two tools touch the same file.
The classic case is a generated config. A build step regenerates settings.json on every deploy. Library A iterates keys in insertion order; library B upgraded and now iterates in a different order. Suddenly every commit shows forty moved lines, and reviewers tune out. When real change hides in noise, people stop reading diffs carefully, and that is where regressions slip through.
Alphabetical order removes the variable. If both writers, or a sort pass after either writer, always produce keys in the same order, the file's textual form depends only on its data. Same data in, byte-identical file out.
Recursive alphabetical sort stabilizes git diffs
Sorting only the top level is half a fix. Config objects nest: a server block holds a tls block holds a ciphers list. If the inner blocks keep their accidental order, the same phantom-diff problem just moves one level down.
The concrete rule that makes diffs stable is recursive alphabetical sort: walk every object at every depth, including objects nested inside arrays, and reorder each one's keys the same way. After a recursive pass, two structurally identical documents serialize to identical text no matter what produced them. That single property, deterministic output regardless of input order, is what turns a noisy diff into a one-line diff.
Values stay untouched. Sorting reorders keys; it never edits numbers, strings, booleans, or null. 42 stays 42, 3.14 stays 3.14, and a string is copied verbatim. The JSON Sort Keys tool does exactly this, parsing with the browser's own JSON engine and re-serializing, so running it twice gives you the same bytes both times.
A worked example: unsorted in, sorted out
Here is a small config with keys in arrival order and one nested object:
{
"server": { "port": 8080, "host": "0.0.0.0" },
"name": "billing-api",
"app": { "retries": 3, "debug": false },
"version": "2.1.0"
}
After a recursive alphabetical sort:
{
"app": { "debug": false, "retries": 3 },
"name": "billing-api",
"server": { "host": "0.0.0.0", "port": 8080 },
"version": "2.1.0"
}
Top-level keys now run app, name, server, version. Inside server, host comes before port; inside app, debug comes before retries. Every value is exactly what it was. Run any other copy of this config through the same sort and it lands in this identical shape, so a diff between the two shows nothing unless a value differs.
Note what does not move: arrays keep their order by default, because position usually carries meaning. ["start","build","deploy"] is a sequence, and alphabetizing it to ["build","deploy","start"] would change what the data says. Sorting plain-value arrays is opt-in for the rare case where order is incidental.
Snapshot tests and caching want canonical form
Two more places deterministic JSON earns its keep.
Snapshot tests. A backend returns JSON with no guaranteed key order. Your snapshot test serializes the response and compares it to a saved file. Because order drifts between runs or between machines, the test flaps green and red on identical data. Sort the response keys before saving the snapshot and before comparing, and the test fails only when a value genuinely differs. The same trick keeps fixture files stable when teammates regenerate them on different machines.
Caching and hashing. If you hash a JSON payload to use as a cache key, two semantically identical objects with different key orders hash to different strings, and you get cache misses on data you already computed. Sort keys first to produce a canonical form, hash that, and equal data always maps to the same key. Pick minified output so the canonical string stays compact.
How I use it in practice
I keep a sort pass wired into my pre-commit hook for a handful of generated config files. Before this, our infra/ directory was the worst offender in code review: every Terraform-adjacent JSON output reshuffled its keys on regeneration, and pull requests routinely showed 200-line diffs where maybe three values had actually changed. I spent more time than I'd admit scrolling to find the real edit.
Adding an alphabetical key sort to the generation step fixed it in an afternoon. Now those files only change when their content changes, and review on them takes seconds instead of minutes. When I'm spot-checking a one-off file by hand, I paste it into the JSON Sort Keys tool, copy the sorted result, and commit that. For comparing two versions of a config after sorting, the JSON diff tool shows the genuine delta cleanly because both sides already share key order.
Watch the two settings that surprise people
Two toggles change the result, and both trip people up.
Recursion. It's on by default, which is what you want for whole-file determinism. But if a nested section has a deliberate hand-tuned order you need to preserve, turn recursion off so only the top-level keys move and the inner structure stays exactly as written.
Case sensitivity. With it off, "Name", "name", and "NAME" group together as the same letters, which reads naturally. Turn it on and uppercase sorts separately from lowercase by code point, so "Name" lands before "name". Match whatever your downstream tooling uses to compare strings. If a config consumer does a case-sensitive key comparison, sort case-sensitively so your canonical form agrees with its view of the data.
The takeaway
Phantom diffs, flapping snapshots, and cache misses share one root cause: JSON key order is undefined, so the same data produces different text. A recursive alphabetical sort pins the text to the data. Wire it into the steps that generate or save JSON, keep arrays in place unless you mean to sort them, and your diffs will only ever show what actually changed.
Made by Toolora · Updated 2026-06-13