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.