JSON syntax rules: the complete guide
JSON’s entire grammar fits on a napkin — that’s the point of it. But the details (what exactly a number may look like, which escapes exist, what happens with duplicate keys) are where documents break and parsers disagree. This is the whole format, edge cases included.
One document, one value
A JSON document (per RFC 8259, the current standard) is exactly one value of one of six types:
| Type | Example | Notes |
|---|---|---|
| object | {"a": 1} | Unordered name/value pairs; names are strings |
| array | [1, "two", null] | Ordered; values may mix types |
| string | "héllo\n" | Double quotes only; Unicode; escape sequences below |
| number | -12.5e3 | Decimal only; no NaN/Infinity; rules below |
| boolean | true, false | Lower-case only |
| null | null | Lower-case only |
Any value may be the top level — "hello" or 42 alone is a valid JSON document. (This surprises people who believe a document must be an object or array; that restriction existed in older specs and some strict parsers still enforce it.)
Strings
Strings are wrapped in double quotes — never single quotes — and may contain any Unicode character except an unescaped ", \, or control character (U+0000–U+001F). The complete set of escapes:
\" quotation mark \b backspace
\\ backslash \f form feed
\/ forward slash (optional) \n line feed
\t tab \r carriage return
\uXXXX Unicode code unit: "\u00E9" is "é"Two subtleties: characters outside the Basic Multilingual Plane (like emoji) are written as a surrogate pair of two \u escapes — "\uD83D\uDE00" is "😀" — though writing the character directly in UTF-8 is also fine. And a real tab or newline typed inside a string is invalid — it must be \t or \n.
Numbers
A JSON number is a decimal number, full stop. The grammar allows:
42 -17 3.14 -0.5
6.022e23 1E-9 0 -0And forbids all of these, each of which some language accepts:
0xFF— no hex, octal, or binary literals042— no leading zeros.5and5.— digits required on both sides of the point+1— no leading plusNaN,Infinity— not numbers in JSON; usenullor a string
The spec sets no precision limit, but in practice most parsers use 64-bit floats: integers beyond ±253 (like many database IDs and timestamps in nanoseconds) silently lose precision in JavaScript. APIs commonly send big IDs as strings for exactly this reason.
Objects and the duplicate-key trap
Object member names must be strings, and the spec says they should be unique — but doesn’t make duplicates a syntax error. So this parses everywhere:
{ "mode": "dev", "mode": "prod" }…and what you get is undefined behavior: most parsers keep the last value, some keep the first, some error. Never rely on it; treat a duplicate key as a bug. Also note that object member order is not guaranteed to survive a parse/serialize round trip, so never encode meaning in the order of keys.
Whitespace, encoding, and other document rules
- Whitespace (space, tab, newline, carriage return) is free between tokens — that’s all a “formatter” or “minifier” changes. Try it: format and minify are the same data.
- JSON exchanged between systems must be encoded as UTF-8 (RFC 8259 §8.1), without a byte-order mark. A BOM is the classic invisible parse error on Windows-edited files.
- No comments. Not
//, not/* */, not#. This was a deliberate design decision to keep the format free of parser directives. - No trailing commas, anywhere. The number-one source of invalid JSON — see our guide to fixing common JSON errors.
“But my JSON file has comments and it works!”
Then it isn’t JSON — it’s one of the friendlier supersets, and the distinction matters when a strict parser rejects your file:
| Format | Adds | Where you meet it |
|---|---|---|
| JSONC | Comments, usually trailing commas | VS Code config files (settings.json, tsconfig.json) |
| JSON5 | Comments, single quotes, unquoted keys, hex numbers, trailing commas | Some JS tooling configs |
| NDJSON / JSON Lines | One JSON value per line, many per file | Logs, data pipelines, LLM training data |
The rule of thumb: write strict JSON whenever another program will read it, and only rely on superset features when the consuming tool explicitly documents support.
What JSON has no type for
The six types on this page are the entire type system, which means several everyday kinds of data have no native representation. Each has a settled convention, and knowing them prevents a lot of interoperability pain:
| Data | Convention | Watch out for |
|---|---|---|
| Dates and times | An ISO 8601 string: "2026-08-11T14:30:00Z" | There is no date type, so every parser hands you a string. Local times without an offset are ambiguous — always include the Z or an explicit offset. |
| Large integers | A string: "9007199254740993" | Above 253 a JSON number loses precision silently in JavaScript. This is why database and social-media IDs arrive quoted. |
| Decimal money | An integer count of minor units (1995 = €19.95), or a string | Binary floating point cannot represent 0.1 exactly. Never store currency as a JSON number if exactness matters. |
| Binary data | Base64 in a string | Costs about 33% in size. For anything large, send a URL instead of the bytes. |
| Comments | A "_comment" key, or an adjacent .md file | Ugly but portable. The alternative is JSONC, which not every consumer accepts. |
| Not-a-number / infinity | null, or a sentinel string | JSON.stringify(NaN) quietly produces null, so the distinction is lost on write, not on read. |
Two standards, one format
JSON is specified twice, which occasionally causes confusion in documentation.RFC 8259 (IETF, 2017) is the version to cite: it supersedes RFC 7159 and 4627, mandates UTF-8 for data exchanged between systems, and permits any value at the top level. ECMA-404 defines the same grammar from the syntax side and deliberately says nothing about encoding or semantics. They do not conflict — ECMA-404 describes what JSON is, RFC 8259 adds requirements for interchanging it. Where a parser differs from either, it is almost always by being more permissive: accepting trailing commas, comments, or a leading byte-order mark that the specification does not allow.
Validate against the real grammar
Indentio’s JSON validator enforces exactly the rules on this page and points at the offending line and column when one is broken — and theformatter’s auto-fix converts most superset syntax (single quotes, unquoted keys, trailing commas) into strict JSON in one click, right in your browser.