JSON has exactly six data types — and that's the whole spec. The simplicity is its strength: any language can implement it. But each type has subtle rules, especially around numbers and Unicode, that bite people who don't read the spec carefully.
The six types
From RFC 8259:
- string — a sequence of zero or more Unicode characters in double quotes
- number — an integer or floating-point in decimal notation
- boolean — literal
trueorfalse - null — the literal
null - object — unordered collection of string-keyed name/value pairs in braces
- array — ordered list of values in brackets
That's everything. No dates, no integers separate from floats, no binary, no comments, no undefined. If you need those, you serialize them — usually as strings.
Strings
Strings are in double quotes. Single quotes are not allowed (a common reason JSON fails to validate). Inside the string, these characters must be escaped: \", \\, \/ (optional), \b, \f, \n, \r, \t, and any character below U+0020 as \u00XX.
Strings can contain any Unicode character. UTF-8 is the default and recommended encoding. The spec also allows \uXXXX escape sequences for characters outside the BMP, encoded as surrogate pairs: "\uD834\uDD1E" is the musical G clef (U+1D11E).
Numbers — where the trouble lives
JSON has one number type, written in decimal notation: 42, -3.14, 1.5e10, 0. The spec does not distinguish integers from floats. It also says nothing about precision or range — a parser can use whatever numeric representation it wants.
In practice this means: large integers can lose precision in JavaScript. JS numbers are IEEE 754 doubles with 53 bits of integer precision. The integer 9007199254740993 (which is 2^53 + 1) can't be represented exactly — JSON.parse('{"id": 9007199254740993}').id returns 9007199254740992. The number was silently rounded.
If your IDs are 64-bit integers (Twitter and Stripe both use them), don't send them as JSON numbers. Send them as strings: {"id": "9007199254740993"}. Every language can preserve a string exactly.
Other number rules: no leading zeros except for 0 itself. No trailing decimal point (5. is invalid). No + prefix (+5 is invalid). NaN and Infinity are not valid JSON — though some implementations accept them as extensions.
Booleans and null
The literals true, false, and null are lowercase. True, TRUE, None, and nil are not valid JSON. Languages that use other spellings (Python's True, Ruby's nil) handle this in their serializers.
null vs missing: these are different. {"x": null} says "x exists and its value is null." {} says "x doesn't exist." APIs frequently conflate them and create confusion. JSON Schema treats them differently: a property listed in required must exist, but it can still be null unless you also exclude the null type.
Objects
Objects are unordered collections of name/value pairs. Keys must be strings (you can't use a number as a JSON object key — write {"1": "x"}, not {1: "x"}).
The spec says keys "SHOULD be unique." Most parsers when given duplicate keys keep the last value: {"a": 1, "a": 2} parses to {"a": 2}. But this is not guaranteed — some parsers reject duplicates entirely, some keep the first. Don't rely on either; just don't emit duplicates.
Order is also not guaranteed. JSON.stringify({"b": 1, "a": 2}) happens to preserve insertion order in modern JavaScript, but the spec doesn't require any tool to preserve or respect order. If you need order, use an array.
Arrays
Arrays are ordered lists, comma-separated, in brackets. Elements can be any JSON value, including other arrays and objects. Arrays can be empty ([]) but cannot have trailing commas ([1, 2,] is invalid in standard JSON, though JSON5 and JSONC allow it).
There's no rule that array elements be the same type — [1, "two", true, null, {"a": 1}] is valid. But if you're designing a schema, mixed-type arrays usually indicate you should be using an object or a tagged union instead.
What JSON doesn't have
No dates. The convention is ISO 8601 strings: "2026-01-15T10:30:00Z".
No binary. Encode it as Base64 in a string.
No comments. JSONC (used by VS Code config) and JSON5 add them, but standard JSON doesn't. If you need comments, put them in a sibling property: {"port": 8080, "_comment_port": "default HTTP"}.
No undefined. Use null or omit the key entirely.
Wrap-up
Six types, a handful of edge cases, and JSON's job is done. The two areas where you'll get hurt are number precision (use strings for large IDs) and null-vs-missing (be explicit about which one means what in your API). Get those right and the rest is mechanical.