Data Formats (JSON, YAML, TOML, XML, CSV) Module 2 – JSON in depth
JSON syntax and data model (RFC 8259)
Write valid JSON for all seven kinds of value, read the RFC 8259 grammar for strings, numbers and whitespace, and spot the near-JSON that parsers refuse.
What you will learn
- Write valid JSON for every value type and recognise invalid near-JSON
- Read the RFC 8259 grammar for strings, numbers and whitespace
- Encode non-ASCII text in JSON strings with and without \u escapes
- Explain how RFC 8259 and ECMA-404 differ and why both matter
Before you start
What you’ll learn · Before you start
What you will learn
- Write valid JSON for every value type and recognise invalid near-JSON
- Read the RFC 8259 grammar for strings, numbers and whitespace
- Encode non-ASCII text in JSON strings with and without \u escapes
- Explain how RFC 8259 and ECMA-404 differ and why both matter
Before you start
On this page
Twelve snippets that look like JSON, from a trailing comma to NaN, all fail RFC 8259’s grammar, yet Python’s json
module accepts one of them and Node.js accepts none. JSON’s grammar is small enough to learn exactly, and that is the
only way to tell valid JSON from text that merely resembles it.
You will write every kind of JSON value, read the grammar of strings, numbers and whitespace closely enough to predict what a parser refuses, encode text outside ASCII with and without escapes, and see what RFC 8259 adds to ECMA-404’s grammar.
JSON has seven kinds of values, and any of them can be a whole document
A JSON text: One complete JSON document: a single value with optional whitespace around it. Since RFC 7159 the value may be of any kind, not only an object or an array. is one value with optional whitespace around it. The value is an object, an
array, a string, a number, or one of the three literals true, false and null. RFC 4627, the first JSON RFC,
allowed only an object or an array at the top; RFC 7159 changed the definition so that a JSON text can be any value,
and RFC 8259 kept that. Each of these seven texts is a complete document:
Python · values/values.py
"""Seven JSON texts, one of each kind of value, each a whole document on its own."""
import json
texts = ['{"branch": "Pune"}', '["BK-101", "BK-205"]', '"Pune"', "37900", "true", "false", "null"]
for text in texts:
value = json.loads(text)
print(f"{text:<22} -> {type(value).__name__:<5} {value!r}") Output
{"branch": "Pune"} -> dict {'branch': 'Pune'}
["BK-101", "BK-205"] -> list ['BK-101', 'BK-205']
"Pune" -> str 'Pune'
37900 -> int 37900
true -> bool True
false -> bool False
null -> NoneType None
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 values.py
JavaScript · values/values.mjs
// The same seven JSON texts in JavaScript: each is a whole document on its own.
const texts = ['{"branch": "Pune"}', '["BK-101", "BK-205"]', '"Pune"', '37900', 'true', 'false', 'null'];
for (const text of texts) {
const value = JSON.parse(text);
const kind = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
console.log(`${text.padEnd(22)} -> ${kind.padEnd(7)} ${JSON.stringify(value)}`);
} Output
{"branch": "Pune"} -> object {"branch":"Pune"}
["BK-101", "BK-205"] -> array ["BK-101","BK-205"]
"Pune" -> string "Pune"
37900 -> number 37900
true -> boolean true
false -> boolean false
null -> null null
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node values.mjs
Runs on this device, in your browser. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Each kind lands on a type of the language that parses it, and the types do not match one for one:
| JSON | Python | JavaScript |
|---|---|---|
object {"branch": "Pune"} |
dict |
object |
array ["BK-101", "BK-205"] |
list |
Array |
string "Pune" |
str |
string |
number 37900 |
int, or float with a fraction or exponent |
number (binary64) |
true, false |
bool |
boolean |
null |
None |
null |
An object is a set of member: One name and value pair inside a JSON object, such as "qty": 2. The name is always a string in double quotes, and a colon separates it from the value., each a string name, a colon and a value, separated by commas
inside braces; an array is a list of values in brackets. Names are always strings in double quotes, so {qty: 2} is not
JSON, whatever a JavaScript object literal allows. Python made 37900 an int, while JSON.parse turns every JSON
number into a JavaScript Number, a binary64 float; the numbers lesson of this module follows that difference to its
consequences.
Expecting a top-level object every time
Wrong: code that does json.loads(body)["status"] because “JSON is an object”. Right: check the type first, since
42, "ok" and null are all valid JSON texts, and a server may send any of them.
Checkpoint
Check: The seven kinds of JSON value
Strings take double quotes, and eight characters have short escapes
A JSON string is text in double quotes. Inside it, the quotation mark, the reverse solidus (backslash) and the
control characters U+0000 to U+001F must be escaped. RFC 8259 gives eight characters short
escape sequence: A backslash and the characters after it, standing for one character inside a string, such as \n for a line feed or \u0041 for the letter A.: \", \\, \/, \b, \f, \n, \r and \t; the solidus,
/, is among them although it never has to be escaped. Every character that need not be escaped may be written as
itself, and any character at all as \u with four hexadecimal digits; a character above U+FFFF then takes two such
escapes, a surrogate pair. This program writes one note that needs most of the rules, then parses escapes back:
"""How json.dumps writes a string that needs escapes, and what json.loads refuses inside a string."""
import json
note = 'Pune, पुणे: "₹379" \\ tab\there, line\nbreak, bell\x07'
print(json.dumps(note)) # ensure_ascii=True, the default
print(json.dumps(note, ensure_ascii=False)) # the text as it is, escaped only where JSON requires it
print(repr(json.loads(r'"\u20b9 \ud83d\udcda \/ \b"'))) # \u escapes, a surrogate pair, \/ and \b
for text in ['"tab\there"', r'"\x41"', "'single'"]:
try:
json.loads(text)
except json.JSONDecodeError as e:
print(f"{text!r:<14} {e}") Output
"Pune, \u092a\u0941\u0923\u0947: \"\u20b9379\" \\ tab\there, line\nbreak, bell\u0007" "Pune, पुणे: \"₹379\" \\ tab\there, line\nbreak, bell\u0007" '₹ 📚 / \x08' '"tab\there"' Invalid control character at: line 1 column 5 (char 4) '"\\x41"' Invalid \escape: line 1 column 2 (char 1) "'single'" Expecting value: line 1 column 1 (char 0)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 escapes.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The first line is json.dumps with its default ensure_ascii=True: every character outside ASCII became a \u
escape, so the Devanagari name and the rupee sign, U+20B9, turned into six-character escapes. The second line, with
ensure_ascii=False, kept them as they are, and escaped only what JSON requires: the quotation marks, the backslash,
the tab, the line break and the bell character, U+0007, which has no short escape and became \u0007. Both lines are
valid JSON for the same string.
The three refusals are the rules from the other side: a raw tab inside a string is a control character, \x41 is not
one of JSON’s escapes, and single quotes do not start a string. JavaScript writes strings the way ensure_ascii=False
does and counts the surrogate pair as two code units:
// The same string through JSON.stringify, which escapes only what JSON requires, and three strings JSON.parse refuses.
const note = 'Pune, पुणे: "₹379" \\ tab\there, line\nbreak, bell\x07';
console.log(JSON.stringify(note));
const parsed = JSON.parse('"\\u20b9 \\ud83d\\udcda \\/ \\b"');
console.log(JSON.stringify(parsed), `is ${parsed.length} UTF-16 code units`);
for (const text of ['"tab\there"', '"\\x41"', "'single'"]) {
try {
JSON.parse(text);
} catch (e) {
console.log(JSON.stringify(text).padEnd(16), `${e.name}: ${e.message}`);
}
} Output
"Pune, पुणे: \"₹379\" \\ tab\there, line\nbreak, bell\u0007" "₹ 📚 / \b" is 8 UTF-16 code units "\"tab\there\"" SyntaxError: Bad control character in string literal in JSON at position 4 (line 1 column 5) "\"\\x41\"" SyntaxError: Bad escaped character in JSON at position 2 (line 1 column 3) "'single'" SyntaxError: Unexpected token ''', "'single'" is not valid JSON
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node escapes.mjs
Choose ensure_ascii on purpose
Keep the default ensure_ascii=True when the bytes may pass through a system that mangles non-ASCII text, and use
ensure_ascii=False for files people read, where पुणे is clearer than \u092a\u0941\u0923\u0947; the file is then
UTF-8, as RFC 8259 asks.
Checkpoint
Check: Strings and their escapes
Numbers follow a strict grammar: no plus sign, no leading zero, no NaN
RFC 8259 writes a number as an optional minus sign, an integer part, an optional fraction and an optional exponent, in base 10. Leading zeros are not allowed, and values that the grammar cannot write, such as Infinity and NaN, “are not permitted”. Step through one number that passes and four that do not, one part of the grammar at a time:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- The sign of -0.5e+3. The number starts with a minus sign, which the grammar allows once, at the very start.
- The integer part. Next comes 0. A zero is a whole integer part by itself, so the integer part ends here and the next character must start the fraction or the exponent.
- The fraction. A full stop starts the fraction, and at least one digit must follow it: here 5.
- The exponent. e starts the exponent, + is its optional sign, and 3 its digits. Nothing follows, so -0.5e+3 is a valid number, with the value -500.
- 012: a leading zero. 0 ends the integer part, so the 1 that follows is extra. Python reported Extra data at char 1, and Node.js Unexpected number at position 1.
- +1: a plus sign. Only a minus may come first. Both parsers refused the +, Python with Expecting value at char 0.
- .5: no integer part. The integer part is not optional, so a number cannot start with a full stop; 0.5 is the way to write it.
- 1.: an empty fraction. A full stop must be followed by at least one digit, so 1. is not a number, while 1 and 1.0 are.
Most near-numbers fail at a single part: 012 and 0x1F at the integer part, which ends after a zero, +1 at the
sign, .5 because the integer part is required, 1. because a fraction needs a digit. The grammar also has no room
for digit separators such as 1_000, which Python source allows, or for digits of other scripts. Python’s json
module accepts NaN, Infinity and -Infinity anyway, as its documentation says, and writes them too, which makes
its output invalid for every strict reader; the next section shows the accepted NaN.
Key idea
A JSON number is an optional minus, then 0 or a digit from 1 to 9 followed by digits, then optional fraction and exponent parts; anything else, including NaN and Infinity, is not JSON, even where a parser accepts it.
Checkpoint
Check: The number grammar
Four whitespace characters, and the near-JSON that parsers refuse
Between tokens JSON allows exactly four whitespace characters: space, horizontal tab, line feed and carriage return. Everything else outside strings is structure: braces, brackets, the colon, the comma, and the literals and numbers. That leaves no room for comments, trailing commas, single quotes or unquoted names. Here are twelve snippets that people write when they half-remember JSON, through Python’s parser:
"""Twelve near-JSON snippets through Python's json module: what it accepts, and what it says when it refuses."""
import json
snippets = [
'{"qty": 2,}', "[1, 2,]", "{'qty': 2}", "{qty: 2}", '// a comment\n{"qty": 2}', "NaN",
"012", "+1", ".5", "0x1F", "True", '{"a": 1} {"b": 2}',
]
for text in snippets:
try:
value = json.loads(text)
print(f"{text!r:<26} accepted: {value!r}")
except json.JSONDecodeError as e:
print(f"{text!r:<26} {e.msg} (char {e.pos})") Output
'{"qty": 2,}' Illegal trailing comma before end of object (char 9)
'[1, 2,]' Illegal trailing comma before end of array (char 5)
"{'qty': 2}" Expecting property name enclosed in double quotes (char 1)
'{qty: 2}' Expecting property name enclosed in double quotes (char 1)
'// a comment\n{"qty": 2}' Expecting value (char 0)
'NaN' accepted: nan
'012' Extra data (char 1)
'+1' Expecting value (char 0)
'.5' Expecting value (char 0)
'0x1F' Extra data (char 1)
'True' Expecting value (char 0)
'{"a": 1} {"b": 2}' Extra data (char 9)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 valid_or_not.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Eleven refusals and one acceptance: NaN, which Python reads as float('nan') although RFC 8259 does not permit it.
012, 0x1F and two objects in a row all ended with Extra data: the parser read one complete value, 0 or the first
object, and then found more text where a JSON text must end.
Test two edge cases
Press Edit, add "1E+05" and "-0" to snippets and run it again: both are accepted, as 100000.0 and 0, because
the leading-zero rule applies only to the integer part and a minus may stand before a zero.
The same twelve snippets through JSON.parse in Node.js:
// The same snippets through JSON.parse. Only accepting or refusing is standard; the wording belongs to the engine.
const snippets = [
'{"qty": 2,}', '[1, 2,]', "{'qty': 2}", '{qty: 2}', '// a comment\n{"qty": 2}', 'NaN',
'012', '+1', '.5', '0x1F', 'True', '{"a": 1} {"b": 2}',
];
for (const text of snippets) {
try {
console.log(`${JSON.stringify(text)}\n accepted: ${JSON.stringify(JSON.parse(text))}`);
} catch (e) {
console.log(`${JSON.stringify(text)}\n ${e.name}: ${e.message}`);
}
} Output
"{\"qty\": 2,}"
SyntaxError: Expected double-quoted property name in JSON at position 10 (line 1 column 11)
"[1, 2,]"
SyntaxError: Unexpected token ']', "[1, 2,]" is not valid JSON
"{'qty': 2}"
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
"{qty: 2}"
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
"// a comment\n{\"qty\": 2}"
SyntaxError: Unexpected token '/', "// a comme"... is not valid JSON
"NaN"
SyntaxError: "NaN" is not valid JSON
"012"
SyntaxError: Unexpected number in JSON at position 1 (line 1 column 2)
"+1"
SyntaxError: Unexpected token '+', "+1" is not valid JSON
".5"
SyntaxError: Unexpected token '.', ".5" is not valid JSON
"0x1F"
SyntaxError: Unexpected non-whitespace character after JSON at position 1 (line 1 column 2)
"True"
SyntaxError: Unexpected token 'T', "True" is not valid JSON
"{\"a\": 1} {\"b\": 2}"
SyntaxError: Unexpected non-whitespace character after JSON at position 9 (line 1 column 10)
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node valid_or_not.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Version note
The Run button runs JavaScript in QuickJS, which refuses the same twelve snippets in words of its own, such as “expecting property name” where Node.js says “Expected double-quoted property name”. Only whether a text is refused is standard; no specification fixes the wording of a parser’s errors.
In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:
"{\"qty\": 2,}"
SyntaxError: expecting property name
"[1, 2,]"
SyntaxError: unexpected token: ']'
"{'qty': 2}"
SyntaxError: expecting property name
"{qty: 2}"
SyntaxError: expecting property name
"// a comment\n{\"qty\": 2}"
SyntaxError: unexpected token: '/'
"NaN"
SyntaxError: unexpected token: 'NaN'
"012"
SyntaxError: Unexpected number
"+1"
SyntaxError: unexpected token: '+'
".5"
SyntaxError: unexpected token: '.'
"0x1F"
SyntaxError: unexpected data at the end
"True"
SyntaxError: unexpected token: 'True'
"{\"a\": 1} {\"b\": 2}"
SyntaxError: unexpected data at the end
Node.js refused all twelve, NaN included. The messages differ from Python’s for every line, and between JavaScript
engines too, so a program should rely on the fact that a parser refused a text, never on its message.
Repairing near-JSON silently
Wrong: stripping comments and trailing commas with a regular expression before every json.loads, which also changes
strings that happen to contain // or ,]. Right: refuse near-JSON at program boundaries, and accept a variant such
as JSONC only where people edit files, with a parser made for it; a later lesson of this module covers those variants.
Checkpoint
Check: Whitespace and near-JSON
Two specifications: ECMA-404 for the grammar, RFC 8259 for exchange
Two documents define JSON, and they agree on the grammar. ECMA-404, The JSON data interchange syntax, now in its second edition, says that its goal is “only to define the syntax of valid JSON texts” and that it provides no semantics. RFC 8259, an Internet Standard, repeats the same grammar and adds what two systems need to agree on to exchange JSON:
| Topic | ECMA-404 | RFC 8259 |
|---|---|---|
| Grammar of values, strings, numbers, whitespace | Defined | The same grammar, in ABNF |
| Encoding between systems | Not covered | UTF-8, without a byte-order mark |
| Names in an object | Not covered | SHOULD be unique; with duplicates, behaviour is unpredictable |
| Numbers | Grammar only | Interoperability is good within IEEE 754 binary64 precision |
| Parsers | Not covered | May set limits on size, nesting depth, numbers and strings |
| Media type | Not covered | Registers application/json |
So ECMA-404 answers “is this JSON?”, and RFC 8259 answers “will the other side read it the way I meant?”. ECMAScript’s
JSON.parse points at ECMA-404 for the grammar and adds one rule of its own: when a name repeats, the later value
overwrites the earlier one. The last two lessons of this module are about the RFC 8259 rows: numbers beyond binary64,
and duplicate names.
Checkpoint
Check: ECMA-404 and RFC 8259
Practise: check numbers against the grammar
The exercise turns the grammar of the number section into code, in Python or JavaScript, with sample tests built from the near-numbers of this lesson: a leading zero, a plus sign, a bare full stop, NaN, hex, digit separators and digits of other scripts:
Exercise · Easy · Python, JavaScript
Check a number against the RFC 8259 grammar
Write is_json_number(text) in number.py (JavaScript: isJsonNumber(text) in number.mjs). It returns True when the whole of text is one number by the grammar of RFC 8259, section 6, and False otherwise:
- an optional minus sign, never a plus;
- an integer part that is
0alone, or a digit from 1 to 9 followed by any digits; - an optional fraction: a full stop and one or more digits;
- an optional exponent:
eorE, an optional+or-, and one or more digits.
The digits are the ASCII digits 0 to 9 only. Nothing may come before or after the number, not even a space. Do not call a JSON parser: Python's json.loads accepts NaN, which is not a JSON number, and the point is the grammar.
Python · Starter code · number.py
import re
def is_json_number(text):
"""Whether the whole of text is one number by the grammar of RFC 8259, section 6."""
return text.isdigit() The sample tests · test_number.py
from number import is_json_number
VALID = ["0", "-0", "7", "10", "37900", "-12", "0.5", "-0.5", "1.25", "1e5", "1E5", "1e+5", "1e-5", "-0.5e+3", "2.5E-10"]
INVALID = ["012", "+1", ".5", "1.", "1e", "1e+", "--1", "-", "", " 1", "1 ", "NaN", "Infinity", "-Infinity", "0x1F",
"1_000", "1.5.2", "1e5.5", "١٢"]
def test_valid_numbers():
"""accepts every form the grammar allows"""
for text in VALID:
assert is_json_number(text) is True, text
def test_invalid_numbers():
"""refuses near-numbers: leading zeros, a plus, a bare dot, NaN, hex, spaces"""
for text in INVALID:
assert is_json_number(text) is False, text
def test_ascii_digits_only():
"""refuses digits of other scripts, which str.isdigit() and \\d accept"""
assert is_json_number("١٢") is False
assert is_json_number("०") is False JavaScript · Starter code · number.mjs
/** Whether the whole of text is one number by the grammar of RFC 8259, section 6. */
export function isJsonNumber(text) {
return /^[0-9]+$/.test(text);
} The sample tests · number.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { isJsonNumber } from './number.mjs';
const VALID = ['0', '-0', '7', '10', '37900', '-12', '0.5', '-0.5', '1.25', '1e5', '1E5', '1e+5', '1e-5', '-0.5e+3', '2.5E-10'];
const INVALID = ['012', '+1', '.5', '1.', '1e', '1e+', '--1', '-', '', ' 1', '1 ', 'NaN', 'Infinity', '-Infinity', '0x1F',
'1_000', '1.5.2', '1e5.5', '١٢'];
test('accepts every form the grammar allows', () => {
for (const text of VALID) assert.equal(isJsonNumber(text), true, text);
});
test('refuses near-numbers: leading zeros, a plus, a bare dot, NaN, hex, spaces', () => {
for (const text of INVALID) assert.equal(isJsonNumber(text), false, JSON.stringify(text));
}); A hint
A regular expression with one group per part of the grammar fits on one line: an optional -, then 0 or [1-9][0-9]*, then an optional \.[0-9]+, then an optional [eE][+-]?[0-9]+, anchored at both ends. Write [0-9] rather than \d: in Python, \d also matches digits of other scripts, such as ١.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then work through the questions on values, strings, numbers, near-JSON and the two specifications; each choice says why it is right or what it misses, and weaker ideas come round first:
Practice that adapts to you
Practise this lesson
15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): is 42 on its own a valid JSON document?
Yes. A JSON text is any single JSON value with optional whitespace around it, so 42, "ok", true and null are
complete documents, just as an object or an array is. The first JSON RFC, RFC 4627, allowed only an object or an array
at the top level; RFC 7159 removed that limit and RFC 8259 kept it out. Python’s json.loads("42") returns the int 42,
and JSON.parse("42") returns the number 42. Some older software still expects an object, which is why RFC 7493’s
I-JSON profile advises protocols to use an object or an array at the top level.
Two harder questions follow, about NaN passing one service and failing another, and about testing a validator’s
number rule. Decide what you would check first, then open each first step:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A partner's feed sometimes sends NaN in a numeric field. Your Python service accepts the feed, and your Node.js service rejects it with a SyntaxError. Explain both behaviours, which one follows the standard, and what you would change on each side.
-
Fresher to mid
A teammate checks the numbers in incoming JSON with a regular expression before parsing. Which inputs would you put in its tests to be sure it follows RFC 8259, and why does the digit class need care in Python?
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What are the seven kinds of JSON value?
Object, array, string, number, true, false and null. Any one of them can be a complete JSON text by itself.
-
Is the text 42 a valid JSON document?
Yes, since RFC 7159. RFC 4627 allowed only an object or an array at the top, and I-JSON still advises one for older receivers.
-
Which characters must be escaped in a JSON string?
The double quote, the backslash, and every character below U+0020. Eight characters have short escapes; any other may appear as itself or as a \u escape.
-
Which number spellings are not JSON?
A plus sign, a leading zero such as 07, a point with no digit on one side such as .5 or 5., hexadecimal, NaN and Infinity.
-
Which whitespace may sit between JSON tokens?
Space, tab, line feed and carriage return, nothing else. Comments, trailing commas, single quotes and unquoted names all make a text invalid.
-
How do ECMA-404 and RFC 8259 divide the JSON rules?
ECMA-404 gives the grammar alone. RFC 8259 adds the exchange rules: UTF-8 between systems, names that should be unique, numbers that interoperate within binary64, and the right of parsers to set limits.
Key takeaways
- A JSON text is a single value of one of seven kinds, object, array, string, number, true, false or null, and since RFC 7159 any of them may be the whole document.
- Strings use double quotes; the quotation mark, the backslash and U+0000 to U+001F must be escaped, eight characters
have short escapes, and
ensure_asciidecides whether other characters are written as\uescapes. - Numbers are an optional minus, then 0 or 1 to 9 followed by digits, then optional fraction and exponent; a plus sign, a leading zero, a bare full stop, hex, NaN and Infinity are not JSON.
- Only four whitespace characters are allowed between tokens, so comments, trailing commas, single quotes and unquoted names make a text invalid; parsers agree on refusing them but not on the wording of their errors.
- ECMA-404 defines the grammar only; RFC 8259 adds the rules for exchange: UTF-8, unique names, binary64 numbers and parser limits.
Terms in this lesson
- escape sequence
- A backslash and the characters after it, standing for one character inside a string, such as \n for a line feed or \u0041 for the letter A.
- JSON text
- One complete JSON document: a single value with optional whitespace around it. Since RFC 7159 the value may be of any kind, not only an object or an array.
- member
- One name and value pair inside a JSON object, such as "qty": 2. The name is always a string in double quotes, and a colon separates it from the value.
References
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format (IETF)
- RFC 7159: The JavaScript Object Notation (JSON) Data Interchange Format, Appendix A (IETF)
- ECMA-404: The JSON data interchange syntax, 2nd edition (Ecma International)
- ECMAScript 2026 Language Specification: The JSON Object (Ecma International)
- json: JSON encoder and decoder (Python 3.14) (Python Software Foundation)
- JSON.parse() (Mozilla)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress