JSON Explained

A plain-language tour of the format that moves data between nearly every system on the web — what it is, how parsers read it, and the edges that bite.

What JSON Actually Is

JSON started as a footnote. In 2001 Douglas Crockford and Chip Morningstar were documenting how they passed data around in a web app, noticed it was just JavaScript object literals with the dangerous parts removed, and wrote it down. A few years later it became RFC 8259. Today it is the default format for APIs, config files, and almost anything two systems need to trade.

The reason is boring and good: JSON has only two structures. A collection of name/value pairs (an object), and an ordered list of values (an array). Everything else is built from those two. That smallness is why a parser can be written in a weekend and why every language ships one.

This guide walks through what JSON is made of, how a parser reads it, where the sharp edges are, and how to keep it safe. The diagrams below are wired to real parsers running in your browser, so what you see is what actually happens, not a cartoon.

The Six Value Types

A JSON value is one of exactly six things. There is no date, no function, no undefined, no comment. If your data needs those, you model them as strings or numbers.

Type Example Notes
string "name" Double-quoted only. No single quotes.
number 36, -1.5, 1e3 One float type. No hex, no leading zero.
object {"k": "v"} Unordered name/value pairs. Keys are strings.
array [1, 2, 3] Ordered. Elements may be mixed types.
true / false true Lowercase literals, no quotes.
null null The only way to say "no value".

The thing people forget: a JSON file must contain exactly one value at the top level. That value is almost always an object or an array. { "a": 1 } is valid; { "a": 1 }{ "b": 2 } is not, because there are two values.

See a Parser Run

Watch a JSON parser read this value

Press play to step through token-by-token. The tree on the right grows as each value is attached.

Source (input)
 
Parsed tree (so far)
 

How a Parser Reads It

JSON is simple enough that you can hold the whole grammar in your head. A parser is a set of rules: "if I see {, read pairs until }; if I see [, read values until ]; if I see a quote, read a string; otherwise read a number, true, false, or null." It calls itself for nested objects and arrays. That is the whole trick.

The diagram below runs a small but real recursive-descent parser over a sample value. Press play and watch the source cursor advance one token at a time while the tree on the right fills in. When it hits "tags", it opens an array; when it hits "active", it reads a boolean; the final null closes the object.

The Rules People Break

Most "broken JSON" errors come from a handful of habits borrowed from JavaScript. JSON is a subset of JS object syntax, not equal to it.

  • No trailing commas. [1, 2,] is fine in JS, a syntax error in JSON.
  • No comments. // and /* */ do not exist in JSON. Put notes in a real field like "_comment" if you must.
  • Double quotes only. Keys and strings use ". Single quotes and unquoted keys are rejected.
  • No unquoted names. { name: "x" } fails; it must be { "name": "x" }.
  • No special numbers. NaN, Infinity, 0xFF, and leading zeros like 01 are all invalid.
  • One root value. Two objects side by side is not a document.

When a parser throws, the position it reports is your friend. A missing comma, an unclosed brace, or a stray trailing comma are the usual suspects, and they almost always sit right where the error points.

See a String Get Escaped

How a string gets escaped

Each character is shown with the token it becomes inside a JSON string. Step through to see why newlines and quotes need a backslash.

Input characters
Escaped output (inside the quotes)
 

Strings and Escaping

A JSON string is any text between double quotes, with one rule that trips everyone: characters that would end or confuse the string have to be escaped first. The quote itself needs a backslash, and so does the backslash. Newlines cannot appear raw inside a string, so they become \n.

Characters below U+0020 (control characters) must be escaped, usually as \uXXXX. The common ones have short forms: \n, \t, \r, \b, \f. Anything above ASCII can be left as the literal character or written as \uXXXX — both round-trip the same.

Step through the next diagram to see each character become its escaped form. Notice that one newline turns into two characters (\ and n), which is why "pretty" strings take more room than they look like they should.

See the Number Traps

JSON numbers are just double-precision floats

Step through surprising results. Every value below is computed live in your browser, not hard-coded.

Numbers: The Quiet Trap

JSON defines numbers loosely, but every parser turns them into an IEEE-754 double (a 64-bit float). That single fact is responsible for most JSON number bugs in production, because doubles are great for physics and bad for accounting and identity.

The diagram below computes each result in your browser. None of it is staged.

JSON Is Not JavaScript

The oldest JSON mistake is to read it with eval(). JavaScript object literals and JSON look alike, but JSON is the safe subset: no variables, no function calls, no assignments. eval() would happily run alert(1) if it were slipped into the data, so it is out of the question.

Use JSON.parse() and JSON.stringify(). They only accept the real grammar and throw on anything else. One more difference worth knowing: JSON.stringify() drops properties whose value is undefined or a function, and turns NaN/Infinity into null. If your object has those, they will not survive the round trip, so normalize first.

Dates do not exist

There is no date type. The convention is to send an ISO-8601 string like "2026-09-04T12:00:00Z" and parse it on the other side. Watch out for timezones — store and send in UTC, format for the user's locale only at display time.

Security: Injection and Pollution

JSON is data, not code, so the classic code-injection bugs do not apply. But two classes of problem do show up, and both are easy to walk into.

JSON inside a page is still data

When you embed JSON into HTML to seed a client app, never drop it raw into a <script> tag. A string containing </script> closes the tag early and lets attacker content run. The safe pattern is to escape < as \u003c (which our own JSON tools do), or serve the data from a separate fetch. If the JSON ever reaches the DOM as markup, escape it first.

Prototype pollution

JSON.parse() itself is safe: it builds plain objects with no prototype tricks, so {"__proto__": {"admin": true}} just becomes a normal property called "__proto__". The danger is in what you do next. Merge helpers, object spreads, and "deep assign" libraries that copy unknown keys onto existing objects can turn that key into a real prototype change. Only JSON.parse the data, then copy the specific fields you expect. Never shallow-merge an untrusted object onto a live one.

See a JSON Bomb Expand

A small file can expand into a huge one

Each level wraps the previous one in 9 copies. Step deeper and watch the expanded value count explode. Computed live.

Nesting, Depth, and the JSON Bomb

JSON can nest without limit, and that is a feature an attacker can use. A small file that expands into a giant structure on parse is called a "JSON bomb" (the XML world calls it a billion-laughs attack). The parser reads a few hundred bytes and then tries to build millions of objects, running the host out of memory.

Step the next diagram. Each level wraps the previous one in nine copies. The source stays tiny, but the number of values it expands into grows by 9× every step. By depth eight it is tens of millions of values from a file you could paste from a chat message.

The defense is boring and correct: cap the input size, cap the nesting depth, and cap the number of nodes you will expand before you give up. Any serious parser or gateway does this; if you are hand-rolling ingestion, do it too.

Validation and JSON Schema

For a config file or an API contract, "it parses" is a low bar. You also want to know the shape is right: the keys exist, the types are correct, the numbers are in range. That is what JSON Schema is for. A schema is itself JSON that describes what valid looks like.

{
  "type": "object",
  "required": ["name", "age"],
  "properties": {
    "name": { "type": "string" },
    "age":  { "type": "integer", "minimum": 0 }
  }
}

Point a validator at your data and it tells you which field broke the contract and why, instead of letting a missing field surface as a confusing error three screens later. For bigger teams, the schema doubles as documentation that cannot drift from the code.

Working With JSON Day to Day

A few habits make JSON painless:

  • Pretty-print while developing, minify in production. Indentation is for humans; whitespace is wasted bytes on the wire.
  • Send big integers as strings. Database IDs and timestamps in nanoseconds do not fit a double. Quote them.
  • Flatten for spreadsheets. Dot-notation (user.address.city) turns nested objects into rows a CSV can hold.
  • Diff before you merge. A structural comparison ignores key order, so it catches real changes, not just reformatting.
  • Generate types from it. One JSON sample can become a TypeScript, Go, or Rust struct, so the wire shape and the code shape stay in sync.

The Mistakes Worth Remembering

Symptom Cause
"Unexpected token" at the end Two root values, or a stray comma before }.
ID changed after a round trip A 64-bit integer was sent as a number and lost precision.
0.1 + 0.2 ≠ 0.3 Float math. Compare with a tolerance, or use integers (cents).
A key vanished on stringify Its value was undefined or a function.
Server chokes on a small payload Nested expansion (JSON bomb). Cap depth and size.

These guides and tools go deeper on the pieces above: