Layerbit
Data Formats

JSON Syntax Errors Explained: What "Unexpected Token" Is Actually Telling You

JSON has one of the shortest grammars of any format in use, which makes it strange that "Unexpected token in JSON at position 217" is such a familiar sight. The grammar is short precisely because it refuses things that look reasonable — a trailing comma, a comment, a single-quoted string — and every one of those refusals has a rationale. This guide walks through the seven mistakes behind nearly every invalid document, how to read the error, and how to find position 217 in a file with no line breaks.

The short version

  • JSON allows: objects, arrays, double-quoted strings, numbers, true, false, null. Nothing else — no comments, no trailing commas, no single quotes, no NaN.
  • "Unexpected token X at position N" means the parser was fine up to byte N and the character there is not allowed by the grammar at that point. The mistake is usually just before it.
  • Position is a character offset from the start, not a line number. Convert it, or use a validator that does.
  • A byte-order mark, a smart quote or a zero-width space is an invisible cause of "Unexpected token" at position 0 or in a string that looks fine.
  • JSON5 and JSONC exist for configuration files that need comments; do not expect JSON.parse to read them.

The whole grammar, on one screen

A JSON value is one of: an object { "key": value, … } with string keys; an array [ value, … ]; a string in double quotes with backslash escapes; a number in decimal notation with optional fraction and exponent; or one of the literals true, false, null. Whitespace between tokens is ignored. That is the complete specification of RFC 8259, and every rule below is a consequence of it being that small.

The seven mistakes

1. Trailing commas

{ "a": 1, "b": 2, }      ✗   Expected double-quoted property name
[1, 2, 3, ]              ✗   Unexpected token ]

JavaScript object literals allow a trailing comma; JSON does not. It is the most common error by a wide margin because it appears whenever a line is deleted from the end of a hand-edited list. The message points at the closing bracket, but the problem is the comma before it.

2. Single quotes

{ 'name': 'Ada' }        ✗   Unexpected token '

Strings and keys must use double quotes. Python's str(dict) and JavaScript's default object printing both produce single quotes, which is how single-quoted "JSON" ends up in log files and gets pasted into a parser.

3. Unquoted keys

{ name: "Ada" }          ✗   Expected double-quoted property name

Legal in JavaScript, not in JSON. Every key is a string and every string is double-quoted.

4. Comments

{
  // the user's display name
  "name": "Ada"
}                        ✗   Unexpected token /

JSON deliberately has no comments — its designer removed them to prevent parsing directives from creeping in. Configuration formats that want them use JSONC (VS Code's settings) or JSON5, which accept comments, trailing commas and single quotes. A standard parser rejects all three, so a tsconfig.json with comments cannot be read by JSON.parse even though TypeScript reads it fine.

5. Values that are not JSON values

{ "ratio": NaN }         ✗   Unexpected token N
{ "when": undefined }    ✗   Unexpected token u
{ "big": 1_000_000 }     ✗   Unexpected token _
{ "hex": 0xFF }          ✗   Unexpected token x
{ "n": .5 }              ✗   Unexpected token .
{ "n": 01 }              ✗   Unexpected number

NaN, Infinity and undefined are JavaScript, not JSON; JSON.stringify turns the first two into null and drops properties holding the third, which is a separate surprise. Numbers must be plain decimal: no leading zeros, no leading dot, no hex, no separators.

6. Control characters and bad escapes inside strings

{ "path": "C:\Users\ada" }     ✗   Bad escaped character
{ "text": "line one
line two" }                    ✗   Bad control character in string literal

Inside a string, a backslash must start one of nine escapes: \", \\, \/, \b, \f, \n, \r, \t or \uXXXX. A Windows path needs doubled backslashes. A literal newline inside a string is not allowed; it must be written \n. Tabs and other control characters likewise.

7. Something outside the document

{"a":1}{"b":2}           ✗   Unexpected non-whitespace character after JSON
                         ✗   Unexpected end of JSON input
\ufeff{"a":1}            ✗   Unexpected token  (at position 0)

A file must contain exactly one value. Two objects back to back are two documents (that is NDJSON, one per line, which needs a different reader). Empty input is not null; it is an error. And a byte-order mark before the opening brace — added by Windows editors and Excel — is an invisible character at position 0 that makes a perfectly good file fail with a message that seems to complain about the brace.

Find the position, see the problem

Layerbit's JSON Validator reports errors with line and column and moves the caret to the offending character, so "position 217" becomes a highlighted comma. It also formats, minifies, queries any path and shows the structure as a tree, entirely in your browser.

Open JSON Validator

Reading the error message

Parsers report the first point at which the input stops matching the grammar. That is not always where the mistake is. Given {"a": 1 "b": 2}, the missing comma after 1 is the bug, but the parser was still happy at the 1 — a number can be followed by whitespace — and only fails at the " that begins "b". So the reported position is after the real mistake, and the habit to build is: look at the token named in the message, then look left.

Engines differ in wording. V8 (Chrome, Node) says Unexpected token '"', …"a": 1 "b": 2}" is not valid JSON in recent versions, or Unexpected string in JSON at position 8 in older ones. Firefox says JSON.parse: expected ',' or '}' after property value in object at line 1 column 9, which is more helpful. Python says Expecting ',' delimiter: line 1 column 9 (char 8). All three are describing the same byte; only Firefox and Python give you a line and column directly.

Turning a position into a line

A position is a zero-based character index from the start of the text. For a pretty-printed file, count newlines before that index to get the line, and the distance from the last newline to get the column. For a minified file there are no newlines, so the position is the column, and the practical technique is to format the document first and validate again — the error moves to a readable line, and the surrounding structure is visible. Beware that the index counts UTF-16 code units in JavaScript and bytes in some other tools, so a document with emoji before the error can put the two off by one or two.

When the file looks valid and is not

If every quote, comma and bracket checks out and the parser still objects, the cause is a character you cannot see. The byte-order mark at position 0 is the classic. Smart quotes “ ” pasted from a document or a chat app look like " and are not — the parser reports an unexpected token at the start of a string that appears perfectly normal. A non-breaking space between tokens is not whitespace to JSON. A zero-width space inside a key makes the key fail to match on lookup rather than fail to parse, which is worse. Inspecting the code points around the reported position resolves all of these in a minute; guessing does not.

Strict by design

Every rejection above is a feature. A format with no comments cannot carry hidden directives. A format with no trailing commas has one canonical serialisation of any value. A format with no NaN and no undefined forces the producer to decide what it meant. That strictness is why JSON parses identically in every language and why a file that is valid today is valid forever. The cost is that hand-editing it is unforgiving — so let a validator do the checking, read the error from the named token leftwards, and keep an eye out for the characters that are not there.

Frequently asked questions

Because the grammar defines an object as key-value pairs separated by commas, with nothing after the last pair. Allowing an optional trailing comma would create two valid spellings of the same value and complicate every parser. JavaScript object literals do permit it, which is why the habit carries over and why it is the most common JSON error.

Not in standard JSON. The format was deliberately specified without comments so that it could not carry parsing directives. Configuration files that need them use JSONC (as in VS Code settings) or JSON5, which accept comments, trailing commas and single quotes, but those must be read with a parser that supports them; JSON.parse and most language libraries will reject them.

The parser accepted everything up to character index N and found a character there that the grammar does not permit at that point. The actual mistake is often just before that position, such as a missing comma between two values. Convert the position to a line and column, or format the document first so the error lands on a readable line, then look at the named token and to its left.

Usually an invisible or lookalike character: a byte-order mark before the opening brace, curly "smart" quotes instead of straight double quotes, a non-breaking space between tokens, or a literal newline or tab inside a string. Inspect the code points near the reported position; a validator that moves the caret to the error makes this quick.

No. Every string and every key must be enclosed in double quotes. Single-quoted output usually comes from printing a Python dictionary or a JavaScript object with the language's default formatter rather than a JSON serialiser; use json.dumps or JSON.stringify to produce real JSON.