The whole grammar
JSON's complete grammar fits on a single page, which is unusual for a data format and entirely intentional. There are six types: four primitives and two containers.
| Type | Example | Rules |
|---|---|---|
| string | "Ada" | Double quotes only; certain characters must be escaped |
| number | 42, -1.5e3 | Decimal only; no leading +, no leading zeros, no hex |
| boolean | true, false | Lowercase only |
| null | null | Lowercase only |
| object | {"a": 1} | Unordered pairs; keys must be quoted strings |
| array | [1, 2] | Ordered; values may be of mixed type |
Under RFC 8259, any of these six may be the top level of a document. 42 and "hello" are each complete, valid JSON. The older RFC 4627 required an object or array at the root; that restriction was removed and the current standard does not have it.
Strings
A JSON string is zero or more Unicode characters wrapped in double quotes. Single quotes are never valid.
Three things must be escaped inside a string:
- the double quote —
\" - the backslash —
\\ - any control character below U+0020
Control characters have short escape forms: \n newline, \t tab, \r carriage return, \b backspace, \f form feed. Any character may also be written as a \uXXXX escape. The forward slash may optionally be escaped as \/ — this is permitted but never required, and exists because </script> inside embedded JSON can terminate an HTML script tag early.
{
"quote": "She said \"hello\"",
"path": "C:\\Users\\Ada",
"multiline": "line one\nline two",
"unicode": "caf\u00e9",
"emoji": "🎉"
}
A literal newline inside a string is invalid — it must be written as \n. This is what produces the "bad control character" error when text is pasted from a file.
Characters outside the Basic Multilingual Plane, such as emoji, may be written literally in a UTF-8 document or as a surrogate pair of \uXXXX escapes.
Numbers
JSON numbers are decimal, optionally signed, optionally fractional, optionally with an exponent. There is no distinction between integer and float — 1 and 1.0 are the same number.
Invalid, despite being valid in many languages:
| Invalid | Why | Write instead |
|---|---|---|
+5 | No leading plus | 5 |
.5 | Needs a leading digit | 0.5 |
5. | Needs a trailing digit | 5.0 |
007 | No leading zeros | 7 |
0x1F | No hexadecimal | 31 |
NaN, Infinity | Not JSON values | null or a string |
1_000 | No separators | 1000 |
The precision trap. The specification does not mandate a numeric precision, but virtually every parser uses IEEE 754 doubles. Integers above 2^53 (9,007,199,254,740,992) silently lose precision:
JSON.parse('{"id": 9007199254740993}').id
// → 9007199254740992 ← the value changed
This bites 64-bit database IDs and snowflake identifiers. The standard fix is to transport large integers as strings and convert at the boundary.
Objects
An object is an unordered set of key/value pairs inside braces. Every key must be a double-quoted string — not an identifier, not a number, not single-quoted.
{
"id": 42,
"name": "Ada",
"address": { "city": "London" },
"roles": ["admin"]
}
Two consequences of "unordered" worth internalising:
Key order carries no meaning. Parsers generally preserve insertion order in practice, but the specification does not require it. Never build logic that depends on it.
Duplicate keys are undefined behaviour. {"a": 1, "a": 2} is not forbidden by the grammar, but the spec does not say which value wins. Most parsers take the last. Some security issues have come from two systems in a pipeline resolving duplicates differently — do not produce them.
An empty object, {}, is valid.
Arrays
An ordered list of values in square brackets. Order is significant. Values may be of mixed type, and may nest:
[1, "two", true, null, {"a": 1}, [2, 3]]
Empty arrays are valid. Trailing commas are not.
How JSON is stricter than JavaScript
Nearly every JSON parse error traces to this gap. JSON's syntax was derived from JavaScript object literals, but the two are not the same language, and JavaScript has grown more permissive since JSON was frozen.
// Valid JavaScript — every line is invalid JSON
{
name: 'Ada', // unquoted key, single quotes
age: 36, // trailing comma below
tags: ['a', 'b',], // trailing comma
// a comment
big: 1_000_000, // numeric separator
nothing: undefined, // not a JSON type
}
{
"name": "Ada",
"age": 36,
"tags": ["a", "b"],
"big": 1000000,
"nothing": null
}
The five rules that cause the overwhelming majority of failures:
- Double quotes only — keys and string values alike.
- No trailing commas — valid in modern JS, invalid in JSON.
- No comments — neither
//nor/* */. - Plain decimal numbers — no
+, no leading zeros, no hex, noNaN, noInfinity. - Lowercase literals only —
true,false,null. NotTrue, notNULL.
Also not JSON types: undefined, Date, RegExp, Map, Set, BigInt, and functions. JSON.stringify drops undefined and functions from objects, and turns them into null inside arrays.
What JSON has no type for
Three things you will need constantly, and the conventions that fill the gap:
Dates. No date type. Use ISO 8601 strings in UTC: "2026-07-29T10:45:00Z". This sorts lexicographically, which is a useful property. Avoid Unix timestamps in public APIs — they are ambiguous about seconds versus milliseconds.
Binary data. No binary type. Base64-encode it into a string, and expect roughly 33% size overhead.
Money. No decimal type, and floats cannot represent 0.1 exactly. Use integer minor units (1099 for £10.99) or a string ("10.99"). Never a float.
JSON Best Practices covers these conventions in more depth.
Checking a document against the rules
The fastest way to test whether something is valid JSON is to format it — a formatter must parse before it can print, so invalid input produces an error rather than output.
For structural validation — required fields, types, permitted values — syntax checking is not enough. See JSON Validation for JSON Schema.