Skip to main content
Reference · 7 min read

Why Sorted JSON Keys Come Out in the Wrong Order

Sort an object alphabetically and numeric-looking keys jump to the front, while Z sorts before _x before a. Both are real rules, and neither is a bug.

Sort the keys of this object and the result surprises most people:

{ "b": 1, "A": 2, "a": 3, "_x": 4, "10": 5, "2": 6, "Z": 7, "Ä": 8, "z": 9 }

// sorted:
{ "2": 6, "10": 5, "A": 2, "Z": 7, "_x": 4, "a": 3, "b": 1, "z": 9, "Ä": 8 }

Two things look wrong. The numbers came first even though nothing asked them to. And Z sorted before _x, which sorted before a. Neither is a bug — they are two different rules, and they apply in that order.

Rule one: integer-like keys are not sorted at all

This one is not about sorting. JavaScript objects define their own iteration order, and it puts array-index-like keys first, in ascending numeric order, before every other key. You can see it without sorting anything:

Object.keys({ b: 1, "2": 2, a: 3, "10": 4 })
// [ "2", "10", "b", "a" ]

Nothing was sorted there. "2" and "10" moved to the front because they look like array indices, and "b" stayed before "a" because the remaining keys keep insertion order. The rule comes from the language specification, so every JavaScript engine does it and JSON.stringify inherits it.

A key counts as integer-like only if it is a canonical non-negative integer string. So "2" and "10" qualify; "02", "-1", "2.5" and "2 " do not, and stay with the ordinary keys. That is why a zero-padded id behaves differently from an unpadded one — worth remembering if your keys are identifiers.

Rule two: sorting compares code units, not letters

For everything that is not integer-like, the default Array.prototype.sort compares strings by UTF-16 code unit — their numeric character values — not by anything resembling dictionary order:

CharacterCode unitConsequence
A–Z65–90All uppercase letters sort first.
_95Underscore falls between the cases.
a–z97–122All lowercase after every uppercase.
Ä196Accented characters sort after plain ASCII entirely.

So Z (90) really does come before _x (95), which really does come before a (97). And Ä lands at the very end, after z, which is wrong in every European language that uses it — in Swedish it belongs at the end of the alphabet, in German it sorts with a, and code-unit order happens to match neither by intent.

The alternative, and why it is not the default

localeCompare sorts the way a person would expect, and gives a completely different answer on the same input:

["Z", "_x", "a"].sort()                          // [ "Z", "_x", "a" ]
["Z", "_x", "a"].sort((x, y) => x.localeCompare(y))  // [ "_x", "a", "Z" ]

Readable — and the wrong tool for a config file. Locale-aware comparison depends on the machine's locale, so the same sort can produce different output on a developer laptop and a CI runner. For a file in version control that is a recipe for diffs that appear and disappear depending on who ran the formatter.

Code-unit order is ugly and stable everywhere, which is the property that actually matters for generated files. That is why it is the default, and why the formatter uses it when you turn on key sorting.

When to sort, and when not to

  • Sort generated files that are regenerated often and committed. A stable order keeps the diffs small and reviewable.
  • Sort both sides when you are about to compare two documents by eye. (A structural diff ignores key order entirely, so you do not need to touch the files at all.)
  • Do not sort package.json, tsconfig.json or anything where convention puts particular keys at the top. Sorting moves name and version into the middle and every reviewer notices.
  • Do not sort configuration where the grouping is documentation — related settings placed together on purpose.

Sorting never changes what a parser sees: key order carries no meaning in JSON, and {"a":1,"b":2} and {"b":2,"a":1} are the same document. The risk is entirely social. A sorted file is an enormous diff, and an enormous diff hides the one line you actually changed.

Sorting is recursive, and arrays are left alone

Sorting applies to every nested object, all the way down. Arrays are never reordered, and that distinction is not a limitation — array order is data. Reordering ["admin", "user"] changes what the document means, while reordering an object's keys does not.