Two stages: tokenizing, then building
Parsing a JSON string happens in two conceptual stages. First, tokenizing (lexical analysis) scans the raw text character by character, grouping it into meaningful tokens: {, }, [, ], :, ,, string literals, number literals, and the keywords true/false/null. Second, a recursive-descent parser walks those tokens and builds an actual in-memory structure — nested objects and arrays — following JSON's grammar: an object is { followed by zero or more key-value pairs, a value can itself be an object or array (hence "recursive"), and so on down to the leaf scalar values.
Input: {"a": [1, 2]}
Tokens: { "a" : [ 1 , 2 ] }
Result: an object with one key "a" mapping to an array [1, 2]
Why JSON.parse() replaced eval()
Long before JSON.parse was standardized, some JavaScript code parsed JSON by literally calling eval() on the string, since valid JSON is (mostly) valid JavaScript object-literal syntax. This was always a serious security risk: eval() executes any JavaScript, not just object literals — a malicious string like alert(document.cookie) (or worse) passed to eval() runs as real code with full access to the page, rather than failing as invalid JSON. JSON.parse() only recognizes JSON's actual grammar and throws a SyntaxError on anything else, with no code execution risk regardless of what the string contains.
What happens on a syntax error
A parser fails at the exact point the token stream stops matching the expected grammar — a missing closing brace, an unquoted key, a trailing comma. Most parsers report the failure with a position (line/column or character offset), though for deeply nested documents, the reported position can be misleading if the actual mistake is much earlier and the parser only detects the resulting mismatch much later in the stream.
Streaming vs. whole-document parsing
Standard JSON.parse() requires the entire string in memory before parsing begins — fine for typical API responses, impractical for gigabyte-scale files. Streaming JSON parsers process the input incrementally, emitting events (or partial objects) as they go, trading a more complex API for the ability to handle documents far larger than available memory.
Common mistakes
- Ever using
eval()on untrusted JSON-like input, even "just this once" — it's a direct code-execution vulnerability, not a parsing shortcut. - Assuming the reported error position is exactly where the mistake is. For nested structures, the parser often only detects the problem once the mismatch becomes structurally unavoidable, sometimes well past the actual typo.
- Trying to
JSON.parse()a multi-gigabyte file in one call. Whole-document parsers load everything into memory at once — use a streaming parser for very large inputs.
FAQ
Why is JSON.parse() safer than eval() for parsing JSON?
Because it only recognizes JSON's specific grammar and throws an error on anything else — eval() executes arbitrary JavaScript, so malicious input passed to it can run as real code.
Does a JSON parser build the whole object before returning?
Standard parsers do, yes — the entire input is read and the full object graph is built in one pass; only specialized streaming parsers process input incrementally.
Why does my JSON error point to a line that looks fine?
Nested structure means a mistake earlier in the document is often only detected once parsing reaches a point where the token stream can no longer match the grammar — the reported position is where detection failed, not necessarily where the actual typo is.
Parse and debug JSON safely with the JSON Toolkit — validation happens entirely in your browser, no eval() involved.