JSON syntax rules: the complete guide

Guide · Updated July 2026 · ~8 min read

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:

TypeExampleNotes
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.5e3Decimal only; no NaN/Infinity; rules below
booleantrue, falseLower-case only
nullnullLower-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           -0

And forbids all of these, each of which some language accepts:

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

“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:

FormatAddsWhere you meet it
JSONCComments, usually trailing commasVS Code config files (settings.json, tsconfig.json)
JSON5Comments, single quotes, unquoted keys, hex numbers, trailing commasSome JS tooling configs
NDJSON / JSON LinesOne JSON value per line, many per fileLogs, 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:

DataConventionWatch out for
Dates and timesAn 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 integersA string: "9007199254740993"Above 253 a JSON number loses precision silently in JavaScript. This is why database and social-media IDs arrive quoted.
Decimal moneyAn integer count of minor units (1995 = €19.95), or a stringBinary floating point cannot represent 0.1 exactly. Never store currency as a JSON number if exactness matters.
Binary dataBase64 in a stringCosts about 33% in size. For anything large, send a URL instead of the bytes.
CommentsA "_comment" key, or an adjacent .md fileUgly but portable. The alternative is JSONC, which not every consumer accepts.
Not-a-number / infinitynull, or a sentinel stringJSON.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.

Keep reading