SnapTools
Developer

JSON for humans: the format, the rules and the errors everyone hits

6 min read · updated 2026-07-21

JSON — JavaScript Object Notation — is how most of the modern internet passes data around: API responses, configuration files, log lines, saved application state. It won not because it is powerful but because it is small. The entire grammar fits in a short section of this page. Yet the same handful of mistakes break JSON everywhere, daily, because the format is stricter than the JavaScript it grew out of. This guide covers the whole language, the rules people actually trip on, and what the error messages really mean.

The entire grammar

A JSON document is one value. A value is one of exactly six things:

  • Object — an unordered set of key–value pairs in braces: {"name": "Ada", "age": 36}. Keys are always strings; values are any JSON value, so objects nest.
  • Array — an ordered list in brackets: [1, 2, 3]. Elements can be any value and can mix types.
  • String — text in double quotes: "hello". Special characters are escaped with a backslash: \", \\, \n (newline), \t (tab), and \uXXXX for arbitrary characters by code point. Real newlines are not allowed inside a string — they must be written \n.
  • Number42, -3.14, 2.998e8. No leading zeros (042 is invalid), no bare dot (.5 must be 0.5), and no NaN or Infinity — those are JavaScript, not JSON.
  • true / false — lowercase, unquoted.
  • null — lowercase, unquoted, meaning "deliberately no value".

That is the whole language. Everything else is these six things nested inside each other. A typical API response is an object containing arrays of objects, three or four levels deep — which is why a formatter that indents the nesting turns an unreadable one-line blob into something you can actually navigate.

The strict rules everyone trips on

JSON looks like JavaScript, and that resemblance causes most of the pain, because JSON forbids things JavaScript allows:

  • Double quotes only. 'single quotes' are invalid, for strings and for keys alike. This is the number-one difference from what JavaScript accepts.
  • Keys must be quoted. {name: "Ada"} is a valid JavaScript object literal and invalid JSON. It must be {"name": "Ada"}.
  • No trailing commas. [1, 2, 3,] fails. This bites hardest in hand-edited config files, where adding an entry to the end of a list and forgetting to clean up the comma is a classic.
  • No comments. Neither // nor /* */ exists in JSON. This was a deliberate design decision to stop comments being abused as parsing directives. It is also why hand-maintained JSON config is unpleasant, and why formats like JSONC (used by VS Code) and JSON5 exist — but a standard parser rejects them.
  • No undefined. JavaScript's undefined has no JSON representation. JSON.stringify silently drops object properties whose value is undefined — a frequent source of "where did my field go?" bugs. If absence must be explicit, use null.
  • One value per document. Two objects side by side ({}{}) are not valid JSON. (Newline-delimited JSON, one document per line, is a separate convention used for logs.)

What the parse errors actually mean

Parser messages are terse, but they map to real causes:

  • Unexpected token ' in JSON at position 2 — single quotes. The parser hit ' where it expected ".
  • Unexpected token } / ] — usually a trailing comma just before the closing bracket, or a genuinely missing value.
  • Unexpected end of JSON input — the document stopped early: an unclosed brace or bracket, or (very common in code) an empty string or truncated API response fed to the parser. If an HTTP request failed and returned an empty body, this is the error you see.
  • Unexpected token < in JSON at position 0 — the "JSON" is actually HTML: an error page, a login redirect, or a 404. The < is the start of <!DOCTYPE html>. Look at the raw response, not the parser.
  • Unexpected token N or similar mid-document — often NaN, Infinity or undefined serialised by something that was not a strict JSON encoder.
  • Control-character errors in strings — a literal newline or tab inside quotes that should have been \n or \t; frequent when text is pasted into a JSON template.

The position number is a character offset from the start of the document, counting from 0. On minified one-line JSON it is nearly useless to eyeball — paste the document into a formatter or validator, which will convert the offset into a line and column and usually point at the exact character.

The number-precision trap

JSON's grammar puts no limit on numbers, but the systems on either end do. JavaScript stores all numbers as 64-bit floating point, which represents integers exactly only up to 2⁵³ − 1 = 9,007,199,254,740,991. Beyond that, integers get silently rounded: parse {"id": 9007199254740993} in a browser and you get 9007199254740992 — no error, just a different number.

This is not academic. Databases hand out 64-bit IDs; Twitter's API famously had to add id_str alongside id because tweet IDs crossed the threshold and JavaScript clients corrupted them. The standard defence is the same one Twitter chose: send large IDs as strings. The same floating-point representation is why 0.1 + 0.2 is not 0.3 — for money, transmit integer minor units (pence, cents) or decimal strings, never binary floats.

JSON versus JavaScript object literals

It is worth stating the relationship plainly, because the names invite confusion. A JavaScript object literal is code: it can use single quotes, unquoted keys, trailing commas, comments, expressions ({total: price * qty}), functions, undefined. JSON is data: a string format with none of those. Almost every valid JSON document happens to be valid JavaScript, but very little casual JavaScript is valid JSON. When code constructs JSON by string concatenation instead of JSON.stringify, the JavaScript habits leak in and a strict parser at the other end rejects the result. Always serialise with a real encoder; never build JSON with string glue.

When CSV beats JSON

JSON is the right default for structured, nested, typed data. But for one specific shape — a flat table, rows and columns, no nesting — CSV is often the better tool:

  • Size. JSON repeats every key on every row; CSV states the headers once. For a million rows, that overhead is enormous.
  • Tooling. CSV opens directly in Excel and Google Sheets, and every data tool ingests it. Handing an analyst a JSON array of objects usually means they convert it to CSV anyway.
  • Streaming. CSV is processed line by line trivially; a single giant JSON array must, naively, be parsed whole.

CSV's weaknesses are the mirror image: no types (everything is text, and spreadsheets mangle leading zeros and long numbers), no nesting, no standard encoding rules beyond the loose RFC 4180, and endless quoting edge cases with commas and newlines inside fields. The pragmatic rule: nested or typed data, JSON; flat tabular data for humans and spreadsheets, CSV — and converting between the two is mechanical in either direction when the data really is flat.

JSON's strictness is easy to resent when a config file fails over one trailing comma. But that strictness is exactly why a JSON document parses identically in every language and every decade of tooling — and a formatter plus a validator turns the strictness from an ambush into a five-second fix.