JSON's sharp edges
The format everyone thinks they know, and the five places it quietly bites. ← Workbench
JSON has a reputation for being boring, and on the surface it earned it: six structural characters, three literals, done. The specification (RFC 8259) is short enough to read over coffee. Yet nearly everyone who has moved JSON between two programming languages has hit at least one bug that felt like the parser was broken. The parsers are fine — the bites all live in the seams between systems, exactly where the spec deliberately leaves room. Here are the five worth knowing, with what the text of the standard actually says about each (and if you just need to look at a file, the JSON formatter here handles the nastier corners directly).
The cliff at 253
The specification defines one number format and pointedly refuses to
distinguish integers from decimals. It even names the safe zone itself:
whole numbers in the range −(253)+1 to (253)−1
"are interoperable", because that is what the 64-bit floating-point
format used by JavaScript — and, honestly, most runtimes — can count
exactly. That boundary is 9,007,199,254,740,992. One past it:
9,007,199,254,740,993 rounds down to ...992 the moment it
becomes a JavaScript Number. Database and message IDs are
routinely 64-bit integers, which is why the classic support ticket
reads "the last digits of the ID are wrong in the browser". The fix is
boring and non-negotiable: treat identifiers as strings at the API
boundary, and only convert for display. Decimals have their own slice
of this: 0.1 + 0.2 is 0.30000000000000004 in
any binary floating-point language, and a JSON file is happy to carry
that lie along.
NaN, Infinity and the silent null
The grammar is explicit: numbers such as Infinity and
NaN are not permitted. What varies is how each
implementation copes when a value isn't a number by the time it is
written. JavaScript serializes both as null — no error, no
warning; an array of sensor readings arrives on the client as
[12.5, null, 9.1] and the third reading has quietly become
"no data". Python takes the opposite road: its json module
happily writes the bare word NaN by default,
producing text the standard forbids, and just as happily reads it back.
So a file can be "valid" on one machine and refuse to load on another.
If you want the error at the moment it happens — the only good time —
Python needs allow_nan=False, and nothing in JavaScript
will ever give it to you.
Duplicate keys: unpredictable by design
Object names "SHOULD be unique", says the specification — a
recommendation, not a rule — and then it describes what happens when
they aren't: behavior is unpredictable. Some implementations keep only
the last pair, some report an error, some hand you all of them. In
practice JavaScript and Python both keep the last, so
{"retries": 1, "retries": 5} is 5 today and whatever the
next parser feels like tomorrow. That vagueness is not a curiosity; a
duplicated key in a permissions file can mean different things to
different services reading the same bytes. The safe rule costs
nothing: a file with a repeated name is a broken file, whatever the
parsers currently do with it.
The date that isn't there
JSON has strings, numbers, booleans, null, arrays and objects — and
that is the entire menu. No date, no time. The ecosystem settled on
conventions instead, and they disagree: ISO 8601 strings like
2026-09-02T08:30:00Z on one endpoint, epoch timestamps on
the next. The epoch flavor hides its own trap — seconds since 1970 and
milliseconds since 1970 differ by a factor of a thousand, and
1756842600 versus 1756842600000 both "look
like a timestamp". And an ISO string without the trailing
Z or offset is local time to one reader and UTC to
another. None of this is written down anywhere in the format itself,
so write it down yourself: pick one convention per API, say which in
the field name, and prefer the ISO form with an explicit
Z.
The strict little grammar
Some of the format's rules are stricter than the eye. Whitespace
means exactly four characters — space, tab, newline, carriage return —
and nothing else. Numbers may not start a fraction bare: +1,
01, .5 and 1. are all invalid
(leading zeros are explicitly not allowed), even though most
programming languages accept every one of them. There are no comments
and no trailing commas — a rude shock for anyone who has grown used to
JavaScript object literals, where both have been legal for years, and
where a hand-edited config can therefore "work" in a script and fail in
a parser. One historical note: early specifications only allowed an
object or array at the top level; since 2014, any value qualifies, so a
bare 42 is a complete JSON text. And the standard has a
warning worth repeating: because the syntax came from JavaScript, it is
possible to parse JSON with eval() — which the
specification calls an unacceptable security risk, not least because
the two grammars differ in corners like the line-separator characters
U+2028 and U+2029, legal in JSON but not in JavaScript.
The BOM and the lone surrogate
Two encoding traps close the list. A byte order mark at the start of
a JSON text is something generators "MUST NOT add" — but files keep
arriving with one, because plenty of editors add it to every UTF-8 file
they save. Parsers "MAY ignore" it, so behavior splits again: some skip
it silently, others refuse the whole file, and the error points at
character one and says nothing about encoding. The file looks perfect
in every editor, which is what makes this one so maddening. The second
trap is rarer but nastier: a string escape like "\uDEAD",
a surrogate half without its partner — the specification's own example,
produced in the wild by code that truncates a string between the two
halves of an emoji. Such text is technically allowed, and what a
receiver should do with it is, in the standard's word, unpredictable.
Modern JavaScript escapes it back out on re-serialization; Python
refuses to encode it to UTF-8 at all. If your data can ever contain
user text, decide on a policy before the first unpaired surrogate
picks it for you.
What the spec doesn't settle
None of this makes JSON a bad format — quite the opposite: it is a
small format that says exactly what it guarantees and honestly names
what it doesn't. Key order in an object is not significant, and
implementations differ on whether they even expose it. Precision beyond
the 253 window is the caller's problem. The practical
checklist that falls out of all five seams is short: identifiers are
strings, dates are ISO 8601 with a Z, no duplicate names
ever, UTF-8 without a BOM, and never let non-finite numbers reach the
serializer. And when a file misbehaves, don't trust your eyes — paste
it into the JSON formatter, which
keeps the original spelling of every number and the original column
order precisely so those questions stay answerable.