JSON Format Rules: The Edge Cases That Actually Bite
Most JSON guides explain the cheerful parts and skip the rules that cause production incidents. This is the other guide: a precise walk through what RFC 8259 actually permits, where parsers quietly disagree, and the inputs that look valid until they corrupt your data. Everything here is grounded in the spec and demonstrated in JavaScript, Python, and Go.
The Exact Data Model
RFC 8259 defines exactly six value types and nothing else. A JSON value is an object, an array, a string, a number, or one of the three literal names true, false, and null. That is the entire grammar. Every well-formed document is built from these.
What people expect to write but the spec forbids:
undefined— not a JSON value. JavaScript'sJSON.stringifydrops object properties whose value isundefinedand convertsundefinedarray elements tonull.- Functions — silently dropped by
JSON.stringify, exactly likeundefined. - Dates — there is no date type. A
Dateserializes to an ISO 8601 string; on parse it stays a string. Round-tripping never restores theDateobject. - Comments — neither
//nor/* */. See the dedicated comments article for the workarounds. - Trailing commas —
[1, 2,]and{"a": 1,}are both invalid. - Single quotes —
'text'is not a string; strings require double quotes.
{
"valid": ["object", "array", "string", 0, true, false, null]
}
If a parser accepts any of the forbidden forms, it is implementing a JSON superset (JSON5, JSONC, Hjson), not JSON. Do not rely on that leniency when another consumer downstream is strict.
Numbers: Where Silent Corruption Lives
A JSON number is an optional minus sign, an integer part, an optional fraction, and an optional exponent. The grammar is stricter than most languages' numeric literals:
- No leading zeros.
0123is invalid.0and0.5are fine;01is not. - No leading
+.+5is invalid. Only a leading-is allowed. - No hex, octal, or binary.
0xFFis invalid. - No
NaN,Infinity, or-Infinity. These are not numbers in JSON, even though IEEE-754 has them.JSON.stringify(NaN)produces the literalnull.
So far these are syntax rules a validator catches. The dangerous part is what the grammar allows: the spec sets no limit on the magnitude or precision of a number. 12345678901234567890 is perfectly valid JSON. The problem is that nearly every parser stores numbers as an IEEE-754 double, which has only 53 bits of integer precision — about 15-16 significant decimal digits. Anything larger is silently rounded to the nearest representable double. No error, no warning.
JSON.parse('{"id": 12345678901234567890}').id;
// 12345678901234568000 <-- the last digits are gone
import json
json.loads('{"id": 12345678901234567890}')
# {'id': 12345678901234567890} <-- Python uses arbitrary-precision int, so this survives
json.loads('{"x": 1.000000000000000001}')['x']
# 1.0 <-- but floats still collapse
package main
import (
"encoding/json"
"fmt"
)
func main() {
var v map[string]interface{}
json.Unmarshal([]byte(`{"id": 12345678901234567890}`), &v)
fmt.Println(v["id"])
// 1.2345678901234568e+19 <-- decoded into float64, precision lost
}
Notice the parsers do not even agree. JavaScript and Go default to a 64-bit float and lose digits; Python's json decodes integers into arbitrary-precision int and preserves them, but still loses fractional precision. The lesson: a value can be valid JSON and still be unsafe to round-trip through a given parser. For IDs and money, serialize large integers as strings, or use a parser mode that yields a big-integer or decimal type. The number-precision article covers the safe patterns in detail.
Strings: Escapes, Newlines, and Surrogate Pairs
Strings must be wrapped in double quotes. Inside, certain characters are mandatory to escape:
\"for a double quote and\\for a backslash.- The control characters U+0000 through U+001F must be escaped — either with a shorthand (
\b,\f,\n,\r,\t) or the generic\uXXXXform.
The trap that surprises people most: a literal, unescaped newline inside a string is invalid JSON. This fails:
{"note": "line one
line two"}
A raw line break is a U+000A control character sitting unescaped inside the string, which the grammar forbids. It must be written {"note": "line one\nline two"}. This is why pasting multi-line text straight into a JSON file so often breaks it.
Beyond ASCII, characters can appear literally in UTF-8 or be escaped as \uXXXX. For characters outside the Basic Multilingual Plane — most emoji, for example — the \u escape must use a UTF-16 surrogate pair, because a single \u only encodes 16 bits. The grinning-face emoji 😀 is U+1F600, which escapes to 😀:
{"reaction": "😀"}
A lone surrogate — one half of the pair without its partner, such as "\uD83D" — is invalid because it cannot represent a real Unicode scalar value. Strict parsers reject it; lenient ones may produce a replacement character or a malformed string. If you build JSON by hand or by string concatenation, this is an easy way to emit garbage.
Duplicate Keys: Valid Syntax, Silent Loss
RFC 8259 says object member names SHOULD be unique — a recommendation, not a hard requirement. That single word causes real bugs: {"a": 1, "a": 2} is technically well-formed JSON, and most parsers accept it without complaint, keeping the last value and discarding the first.
JSON.parse('{"a": 1, "a": 2}').a;
// 2
import json
json.loads('{"a": 1, "a": 2}')
# {'a': 2}
var v map[string]int
json.Unmarshal([]byte(`{"a": 1, "a": 2}`), &v)
// v == map[a:2]
All three keep the last value, so the first is lost with no error. This is exploitable and dangerous: a request validator might read the first a while a backend reads the last, or vice versa, opening a parser-differential security hole. Because the behavior is "SHOULD," you cannot count on any parser to flag it. Treat duplicate keys as a defect in the producer, and validate against them explicitly when the input is untrusted.
Key Ordering: Unordered by Spec, Ordered in Practice
The spec is explicit that a JSON object is an unordered collection of name/value pairs. You are not entitled to rely on key order for correctness. In practice, though, most parsers preserve insertion order, which lulls people into depending on it:
- JavaScript objects preserve insertion order for string keys (integer-like keys are a separate, ordered-first case).
- Python 3.7+ dicts preserve insertion order as a language guarantee, and
json.loadsproduces an ordered dict. - Go is where this bites. Unmarshaling into a
map[string]interface{}loses order entirely, and ranging over a Go map iterates keys in randomized order by design. Unmarshaling into astructinstead preserves the field declaration order on re-marshal.
The practical rule: never let two systems agree on meaning through key order. If you need stable output — for diffs, signatures, or cache keys — sort keys explicitly (json.dumps(obj, sort_keys=True) in Python) and canonicalize, rather than hoping the order survives a round trip through a Go service in the middle.
Top-Level Values: It Doesn't Have to Be an Object
A common myth is that a JSON document must be an object or array at the top level. That was true under the older RFC 4627, but RFC 8259 allows any value as a complete JSON text. Each of these is a valid, standalone JSON document:
42
"hi"
true
null
Modern JSON.parse, Python's json.loads, and Go's json.Unmarshal all accept bare scalars. If you maintain an old API that rejects them, it is enforcing a superseded spec, not the current one. This matters for APIs that legitimately return a bare number or string as the whole response body.
Whitespace, Encoding, and the BOM Trap
Insignificant whitespace — space, tab, line feed, carriage return — is allowed between any two tokens and ignored. You can pretty-print freely without changing meaning.
Encoding is the part that quietly breaks files. RFC 8259 specifies that JSON text exchanged between systems MUST be encoded using UTF-8. And critically: the spec states implementations MUST NOT add a byte order mark (BOM) to the start of the text, and notes that parsers MAY ignore one if present. So a leading BOM (U+FEFF, the bytes EF BB BF) is not part of valid JSON — yet some Windows editors and tools prepend one when saving UTF-8.
JSON.parse('{"a":1}');
// SyntaxError: Unexpected token in JSON at position 0
That error message often sends people hunting for a problem in their data when the real culprit is three invisible bytes at the front of the file. If a file that looks identical to a working one fails to parse, check for a BOM first.
Looks Valid, Isn't: A Before / After
Here is a payload that passes a quick eyeball check and fails every strict parser:
{
'user': "Ada",
"age": 042,
"active": True,
"joined": new Date(),
"tags": ["math", "logic",],
}
Five separate violations are hiding in six lines: single-quoted key ('user'), a number with a leading zero (042), a Python/JS-style capitalized literal (True instead of true), a function call where a value belongs (new Date() is not JSON at all), and two trailing commas. The corrected, valid version:
{
"user": "Ada",
"age": 42,
"active": true,
"joined": "2026-06-25T00:00:00Z",
"tags": ["math", "logic"]
}
The date became an ISO 8601 string — the only way JSON can carry a timestamp — and every literal now matches the grammar exactly. When a document like the first one fails, run it through a validator that reports the precise position of the first error rather than guessing.
The Rules Worth Memorizing
The grammar is small, but its sharp edges are concentrated in a few places: numbers that are valid yet lose precision in a double, strings that need surrogate pairs and forbid raw newlines, duplicate keys that vanish silently, key order you must never trust across systems, and an invisible BOM that breaks an otherwise-perfect file. Know these and you avoid the overwhelming majority of "but it looks fine" JSON bugs.
Related Tools & Resources
Tools
- JSON Validator — Catch the exact violations above and see the precise position of the first syntax error.
- JSON Formatter — Pretty-print and normalize whitespace without changing the document's meaning.
- JSON Schema Validator — Enforce types, required keys, and value constraints beyond raw syntax.
- JSON Token Counter — Measure payload size when deciding whether to inline or stringify large values.
Learn More
- JSON Numbers and Precision — The full story on big integers, doubles, and safe round-tripping.
- JSON Comments — Why comments are forbidden and the practical workarounds.
- Common JSON Mistakes — A broader catalog of the errors that trip people up.
- JSON Syntax Errors — How to read and fix parser error messages quickly.
Related Articles
Formatting JSON: When It Helps, When It Hurts
A practical guide to JSON beautifiers: pretty-print for humans, minify for transport, sort keys for diffs, and the lossy gotchas of round-tripping through stringify.
Common JSON Mistakes and How to Avoid Them
The 20 most common JSON syntax errors developers make, organized by category with examples and fixes.
How to Fix 'Unexpected End of JSON Input' Error
Learn what causes the 'Unexpected end of JSON input' error and how to fix it with practical solutions for JavaScript, Python, and other languages.
How to Open JSON Files: A Practical Guide
Open, view, and troubleshoot JSON files on macOS, Windows, and Linux with real commands for jq, Python, VS Code, and browsers — plus fixes for common errors.