JSON is the lingua franca of the web. It's also riddled with footguns that most tutorials skip over. None of these are bugs in JSON itself — they're places where the spec is silent and implementations differ, or where the format pretends to support something it doesn't. Here are ten that have cost me (and probably you) real time.
1. Large integers lose precision
JSON has one number type. JavaScript stores it as an IEEE 754 double, which means integers above 2^53 (≈ 9 × 10¹⁵) can't be represented exactly.
{ "id": 9007199254740993 }
In JavaScript, that parses to 9007199254740992. Off by one. Twitter's id field famously bit users this way, which is why their API also returns id_str.
Fix: serialize big IDs as strings on the producer side. "id": "9007199254740993". Or use a parser that supports BigInt (Node's JSON.parse with a reviver, or JSON5).
2. Floating-point numbers are not what you typed
{ "price": 0.1 }
That round-trips through most JSON parsers fine. But:
{ "total": 0.1 + 0.2 }
That gives you 0.30000000000000004. Same problem any time you store money as a float.
Fix: store currency as integer cents, or as a string. Never compare floats with ===.
3. Duplicate keys silently win or lose
{ "a": 1, "a": 2 }
The JSON spec says objects "should" have unique keys but doesn't forbid duplicates. Most parsers take the last value (a = 2), some take the first, some throw. None warn.
Fix: if you accept JSON from untrusted sources, run it through a strict validator first. The JSON Formatter flags duplicate keys on validation.
4. There are no comments
This is by design but trips everyone. You can't put // note or /* explanation */ in JSON. Many ad-hoc parsers accept them; the standard one does not.
Fix: if you need comments, use a different format — JSONC (with comments), YAML, or TOML. For configuration, those are usually better choices anyway.
5. Trailing commas crash strict parsers
{ "a": 1, "b": 2, }
Valid in JavaScript, invalid in JSON. Hand-written config files trip this constantly.
Fix: use a formatter to validate before committing. Or use JSON5.
6. Dates aren't a JSON type
JSON has no date type. Dates are serialized as strings, but everyone picks a different shape:
{
"iso": "2024-06-01T12:00:00.000Z",
"unix_seconds": 1717243200,
"unix_ms": 1717243200000,
"rfc2822": "Sat, 01 Jun 2024 12:00:00 GMT"
}
If you don't agree on a format with the other side, you'll spend an afternoon debugging timezones.
Fix: standardize on ISO 8601 with a Z suffix (or explicit offset). Document it. The same applies to currency, durations, and any unit-bearing value.
7. null and missing keys are different
{ "a": null }
vs.
{}
A null value says "this field exists, but has no value." A missing key says "this field was not provided." Many APIs treat them the same; some don't. PATCH endpoints especially care.
Fix: in your API design, decide what each one means, document it, and stick to it. A defensive client checks both.
8. JSON Schema validators differ
Even when you write a JSON Schema to validate inputs, different libraries handle edge cases (additional properties, $ref resolution, regex dialects) differently. Two services using "the same schema" may accept different inputs.
Fix: pin the validator library version on both ends. Run a smoke-test suite that sends known-bad inputs through both.
9. Unicode escapes and lone surrogates
JSON strings can use \uXXXX to encode any UTF-16 code unit. Including lone surrogates — half a UTF-16 surrogate pair — which are not valid Unicode.
{ "broken": "\uD83D" }
That decodes, by spec, to a string containing an unpaired surrogate. Round-trip it through UTF-8 and most languages either replace it with the replacement character � or throw an exception.
Fix: reject lone surrogates at the API boundary. Some parsers offer a strict mode for this.
10. Numbers without quotes look fine until they don't
{ "phone": 1234567890 }
That works until the phone number starts with a 0 or contains a +. The producer sees a number, the consumer sees an integer with leading zeros stripped, and your CSV export is wrong.
Fix: anything that isn't arithmetic should be a string. Phone numbers, postal codes, IDs, version numbers ("1.2.3" is not a number).
A formatter is your friend
Most of these surface the moment you pretty-print and look at the data. Try a payload in the JSON Formatter — it validates, beautifies, and flags duplicate keys. For tabular data, the CSV ↔ JSON converter handles the type-inference dance correctly. For JWT payloads (which are Base64-encoded JSON), the JWT Decoder lays the structure out.
JSON isn't broken. It's just a smaller language than people remember, and the gaps are exactly where production bugs live. Knowing the gaps means you can fill them at the schema layer instead of at 3 AM.