A deliberately tiny format — and the handful of ways it quietly loses your data.
JSON's great virtue is that it is small enough to hold in your head. The entire grammar fits on a postcard, which is why it displaced heavier formats almost everywhere. But a few of its omissions and one inherited numeric limitation cause real, quiet data loss, and they are worth knowing before they cost you an afternoon.
JSON has exactly six kinds of value:
{ } containing zero or more "key": value pairs, comma-separated. Keys must be double-quoted strings.[ ] containing zero or more comma-separated values.true or false, lowercase.null, lowercase.That is the complete list. Everything below follows from what is not on it.
// nor /* */. This was a deliberate decision by the format's author, who observed that comments were being used to carry parsing directives and removed the temptation. It remains the most-missed feature, especially in configuration files.[1, 2, 3,] is invalid. This is a genuinely annoying source of diff noise, because adding a line to the end of a list requires editing the line before it.{'a': 1} is invalid; strings and keys need double quotes.{a: 1} is JavaScript object syntax, not JSON. The two are frequently confused because JSON was derived from JavaScript — but JSON is not a subset of it in every respect, and JavaScript is certainly not a subset of JSON.undefined, NaN, or Infinity. There is no way to represent them, which matters when serialising computed numbers — a division that produced infinity will either throw or be silently converted to null depending on the serialiser.012 and .5 are both invalid; write 12 and 0.5.This is the pitfall that silently corrupts data rather than raising an error.
The JSON specification describes number syntax but deliberately says nothing about precision or range. It is left to the implementation — and the overwhelmingly common implementation, JavaScript's, parses every number into an IEEE 754 double-precision float.
A double represents integers exactly only up to 253 − 1, which is 9,007,199,254,740,991. Beyond that, integers lose precision silently:
Sent: { "id": 9007199254740993 }
Parsed: 9007199254740992 // off by one, no error raised
This is not hypothetical. Sixty-four-bit database identifiers, Twitter-style snowflake IDs, large financial figures in minor units, and high-resolution nanosecond timestamps all exceed the safe range routinely. The value round-trips through a system looking entirely plausible while pointing at the wrong record.
The fix is to transmit such values as strings. {"id": "9007199254740993"} survives intact through any parser. It is why so many APIs return an id and an id_str, which looks like redundancy until you know why it is there.
Floating point brings its own familiar consequence: 0.1 + 0.2 is not 0.3, because none of those values is exactly representable in binary. For money, store minor units as integers — pence, cents — or use a decimal string, and never a float.
{"a": 1, "a": 2} is, strictly speaking, valid JSON. The specification does not say what it means.
Most parsers keep the last occurrence. Some keep the first. Some raise an error. Some — in languages with multi-maps — keep both. Since two systems can therefore disagree about what the same document says, this is occasionally exploited deliberately: a document crafted so a validating service and a consuming service read different values.
Do not rely on any behaviour here. If you are writing JSON, do not emit duplicate keys; if you are consuming it from an untrusted source, consider rejecting documents that contain them.
JSON has no way to express a moment in time, so dates are conventionally strings — and the convention is ISO 8601: "2026-09-02T14:30:00Z".
Use it, and include the offset or a trailing Z for UTC. A string like "2026-09-02 14:30:00" with no zone information is ambiguous, and the receiving system will guess — usually as its own local time, usually wrongly. The time zones guide covers why that guess causes trouble and why future events need a zone identifier rather than a fixed instant.
Unix timestamps are the other common choice and are unambiguous, but they are unreadable to humans and hit the precision ceiling above if expressed in nanoseconds.
JSON strings are Unicode. Characters may appear literally in UTF-8, or as \uXXXX escapes. Characters outside the Basic Multilingual Plane — emoji, many historic scripts — need a surrogate pair of two escapes when written in escaped form.
This creates a real edge case: it is possible to write a lone surrogate, half of a pair, which is valid JSON syntax but not valid Unicode. Parsers disagree on what to do, and such a string can fail when later written to a UTF-8 database. If you are generating JSON by string concatenation rather than with a proper serialiser, this is one of several reasons not to.
The mandatory escapes are the double quote, the backslash, and control characters below U+0020. A raw newline inside a string is invalid — it must be \n.
The specification defines an object as an unordered collection, so no meaning may be attached to key order. In practice most parsers preserve the order they encountered, and JavaScript objects preserve insertion order for string keys, so code that accidentally depends on order often works — until it meets a parser that sorts keys or a language whose maps do not preserve order.
If order matters, use an array. That is what arrays are for.
| Format | Adds | Use for |
|---|---|---|
| JSONC | Comments | Configuration files read by a specific tool |
| JSON5 | Comments, trailing commas, unquoted keys, single quotes, hex | Human-edited configuration |
| NDJSON / JSON Lines | One JSON document per line | Logs and streams — each line parses independently, so a huge file needs no full parse and a truncated file is still mostly usable |
| JSON Schema | A validation vocabulary | Describing and enforcing document structure |
None of these is JSON. Never send them to an interface expecting JSON, and be careful that a config file your editor accepts is not fed to a strict parser elsewhere.
Never parse JSON by evaluating it as code. Passing a document to a JavaScript evaluator will appear to work, since JSON syntax is largely valid JavaScript, and it means any untrusted document can execute arbitrary code. Use the real parser. This has been a known and serious vulnerability class for a long time.
Pretty-print anything that will be diffed or reviewed. Minified JSON is one enormous line, so a version-control diff of a changed value shows the whole file as changed. Two spaces of indentation and one value per line makes changes legible, and the size difference is irrelevant for anything not being served at volume.