Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 15 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7, Ruby 4.0.7 and sql.js 1.14.2
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

Seven JSON texts, one of each kind

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

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

  1. Question 1 of 2 Which of these are complete, valid JSON texts?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • {"qty": 2}
    • null
    • "ok"

    A JSON text is one value with optional whitespace around it: an object, an array, a string, a number, true, false or null. Strings and names always use double quotes.

  2. Question 2 of 2 Why did both parsers refuse {qty: 2}?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: An object's names must be strings in double quotes

    JavaScript object literals allow unquoted names, JSON does not: every member is a string in double quotes, a colon and a value. {"qty": 2} is the JSON form.

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:

Escapes in json.dumps and json.loads Python · strings/escapes.py
"""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

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 rules in JavaScript JavaScript · strings/escapes.mjs
// 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

  1. Question 1 of 2 A JSON string contains a real tab character, typed with the Tab key, between two words. What does json.loads do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It refuses the text, because control characters must be escaped

    escapes.py recorded Invalid control character at char 4 for a raw tab inside a string. Between tokens the same tab is allowed whitespace; inside a string it must be written \t.

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    import json
    print(json.dumps("café"))
    Show the answer: Question 2 of 2

    Answer: "caf\u00e9"

    json.dumps escapes every non-ASCII character by default, as the Devanagari name and the rupee sign showed in escapes.py. Both forms are valid JSON for the same string.

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:

Interactive · stage by stage Reading a JSON number, one part of the grammar at a time Step through -0.5e+3, which is a number, then four texts that are not. Each step says which part of the grammar decides.
Reading a JSON number, one part of the grammar at a time, step 8 of 8. Showing Minus sign (no sign), Integer part ("1" read), Fraction ("." with no digit after it), Exponent (not reached), End of the number (refused). In focus: Fraction.Minus signno signInteger part"1" readFraction"." with no digit after itExponentnot reachedEnd of the numberrefused

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The sign of -0.5e+3. The number starts with a minus sign, which the grammar allows once, at the very start.
  2. 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.
  3. The fraction. A full stop starts the fraction, and at least one digit must follow it: here 5.
  4. 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.
  5. 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.
  6. +1: a plus sign. Only a minus may come first. Both parsers refused the +, Python with Expecting value at char 0.
  7. .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.
  8. 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

  1. Question 1 of 2 Which of these are JSON numbers?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • 0.5
    • -0
    • 1e5

    A JSON number is an optional minus, 0 or a digit from 1 to 9 followed by digits, then an optional fraction and an optional exponent. -0, 1e5 and 0.5 fit; 012, +1 and .5 do not.

  2. Question 2 of 2 json.loads("012") says Extra data at char 1. Why there?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The integer part ended at the 0, so the parser had a complete value and then found the 1

    The grammar lets the integer part be 0 alone, or start with 1 to 9. After 0 the number is complete, so Python reported Extra data, and Node.js Unexpected number at position 1, for the same reason.

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 json.loads Python · nearjson/valid_or_not.py
"""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

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 JavaScript · nearjson/valid_or_not.mjs
// 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

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.

JSON Validator Paste a document and see the line and column of its first error, before any program reads it.

Checkpoint

Check: Whitespace and near-JSON

  1. One question Which of these may appear between tokens of a JSON text?

    Choose one answer.

    Show the answer: One question

    Answer: A horizontal tab

    Only four characters are whitespace in JSON. Everything else outside strings is structure, which leaves no room for comments or trailing commas.

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

  1. One question Where is it written that JSON exchanged between systems must be UTF-8?

    Choose one answer.

    Show the answer: One question

    Answer: In RFC 8259

    ECMA-404 answers whether a text is JSON; RFC 8259 adds what two systems must agree on: UTF-8, unique names, the precision of numbers and parser limits.

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 0 alone, 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: e or E, 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 ١.

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.

  1. Question 1 of 15 What types does json.loads("[1, 1.0, 1e2]") give the three numbers?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: int, float, float

    Python's json module turns integers into int and everything with a fraction or exponent into float. JavaScript makes all three the same binary64 number.

  2. Question 2 of 15 A server answers with the JSON text null. What happens here?

    Read the code, then choose one answer.

    data = json.loads(body)   # body is the text null
    print(data["status"])
    Show the answer: Question 2 of 15

    Answer: TypeError: 'NoneType' object is not subscriptable

    A program that expects an object has to check the type first: null, a number or a string are all valid answers from a JSON point of view.

  3. Question 3 of 15 JavaScript parses [1, 1.0] and writes it back with JSON.stringify. What comes out?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: [1,1]

    A JavaScript Number cannot tell 1 from 1.0, so the round trip changes the text. Python keeps the difference as int and float. Programs should never rely on the spelling of a number surviving a round trip.

  4. Question 4 of 15 Which of these is not a valid escape inside a JSON string?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: \x41

    The eight short escapes are \", \\, \/, \b, \f, \n, \r and \t; every other character is written as itself or as a \u escape. \x41 must be written A or \u0041.

  5. Question 5 of 15 How is the character U+1F4DA written with \u escapes in JSON?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: \ud83d\udcda

    JSON's \u escapes name UTF-16 code units, so a character outside the Basic Multilingual Plane needs a high and a low surrogate. Written as itself, in UTF-8, it needs no escape at all.

  6. Question 6 of 15 The JSON string "\ud83d\udcda" is parsed in Python and in JavaScript. What do len() and .length report?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: 1 in Python, 2 in JavaScript

    Length limits written as "at most 50 characters" mean different things in the two languages. A limit in a schema or an API has to say whether it counts code points or UTF-16 code units.

  7. Question 7 of 15 What does Python's json.loads("NaN") return?

    Choose one answer.

    Show the answer: Question 7 of 15

    Answer: The float nan, although NaN is not valid JSON

    valid_or_not.py recorded accepted: nan. RFC 8259 does not permit NaN or Infinity, so Python's output with allow_nan left at its default can break strict readers.

  8. Question 8 of 15 Is 1E+05 a valid JSON number?

    Choose one answer.

    Show the answer: Question 8 of 15

    Answer: Yes; leading zeros are forbidden in the integer part, not in the exponent

    json.loads("1E+05") gives 100000.0 and JSON.parse gives 100000. Read the grammar part by part: each part has its own rules.

  9. Question 9 of 15 A system writes amounts with a decimal comma: {"amount": 3,50}. What does a JSON parser make of it?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: It refuses it, because after the comma it expects another name

    Decimal separators of the writer's locale never belong in JSON. The amount must be written 3.5, or better, as whole minor units, 350.

  10. Question 10 of 15 Can a JSON document contain comments?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: No; the grammar has no comment syntax

    Comments make a text near-JSON. Configuration formats that need them, JSONC and JSON5, are separate variants with their own parsers.

  11. Question 11 of 15 Why did json.loads refuse {"a": 1} {"b": 2} with Extra data at char 9?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: A JSON text is one value, and a second value followed the first

    One text, one value. Several values in one stream need a format made for it, such as JSON Lines, which a later lesson of this module covers, or an array around them.

  12. Question 12 of 15 A log line reads {'qty': 2, 'gift': True}, written with Python's print() of a dict. What is the right fix?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: Change the writer to json.dumps(), so the log holds JSON

    Near-JSON in logs and files almost always comes from a writer that did not use a JSON serialiser. Repairing it afterwards guesses; fixing the writer removes the guess.

  13. Question 13 of 15 What does ECMA-404 define?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: Only the syntax of valid JSON texts

    ECMA-404 is the grammar; RFC 8259 the grammar plus the rules for exchanging JSON. Both matter: the first decides what is JSON, the second whether two systems will read it the same way.

  14. Question 14 of 15 A JSON text contains {"qty": 1, "qty": 2}. What do the documents say?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: RFC 8259 says names SHOULD be unique; ECMAScript's JSON.parse keeps the later value

    The grammar allows repeated names, RFC 8259 warns against them, and each parser decides what to do. That gap is the subject of the interoperability lesson of this module.

  15. Question 15 of 15 A partner's API sends UTF-16 JSON with a BOM and says it is valid because it passes an ECMA-404 grammar check. What do you answer?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: The grammar is fine, but RFC 8259 asks for UTF-8 between systems, so ask for UTF-8

    "Valid JSON" has two meanings: the grammar of ECMA-404 and the exchange rules of RFC 8259. Between systems, ask for both.

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.

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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_ascii decides whether other characters are written as \u escapes.
  • 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.