Data

Reading a JSON syntax error instead of guessing at it

A parse error carries a line, a column and a character offset, and almost nobody reads them. What each standard JSON message means, with a real broken document for every one.

Every JSON parser reports two things when it fails: a category and a place. The category is boilerplate — Unexpected token, Expecting ',' delimiter — and it is the same sentence for a hundred bytes and for a hundred megabytes. The place was computed from your file and from nothing else, and almost everyone reads past it.

Read the position before the message

A parse error carries three numbers. The line is one-based. The column is one-based and counts from the start of that line, not the start of the file. The offset — Python calls it char, V8 calls it a position — is zero-based and counts every character from the start of the document, one character per newline.

Here is a document with a comma missing at the end of line 3:

{
  "name": "Ada",
  "role": "engineer"
  "team": "docs"
}
Expecting ',' delimiter: line 4 column 3 (char 42)

Where does 42 come from? Line 1 is the opening brace plus a newline: two. Line 2 is sixteen characters plus a newline, which brings the total to nineteen. Line 3 is twenty characters plus a newline: forty. Then two characters into line 4, where the parser is looking at the opening quote of "team". One plus one, sixteen plus one, twenty plus one, two: forty-two.

The column is three for the same character, because a column counts from the start of its own line, so a column without a line means nothing. An offset is only an offset in the text the parser saw: a file with Windows line endings holds two bytes per break where a Unix file holds one, and a byte count and a character offset drift apart by one for every line above the failure.

The form above is Python's and carries all three numbers at once. The form you have seen in a browser console is V8's: Unexpected token '}' in JSON at position 15, one number and no line.

Why the reported position lands one character late

A parser accepts a token the moment it is complete, because at that moment there is nothing to reject. A comma is not malformed; a closing brace is not malformed. Either becomes an error only in the presence of the other, and by then the parser has consumed one of them. That is why the position often points at the character after the one you must change.

A comma left at the end of the only member:

{"name": "Ada",}

Expecting property name enclosed in double quotes: line 1 column 16 (char 15)

Character 15 is the closing brace. The character you delete is 14, the comma, one to its left. The same fault in the other direction — a comma that should be there and is not:

[{"a": 1}{"b": 2}]

Unexpected token '{' in JSON at position 8

Character 8 is the second object's opening brace, and character 8 is where the comma belongs, so the repair is to insert rather than delete. The character at the reported position tells you which case you are in: a closing brace or bracket means the parser found a separator where it wanted the end of something, so delete at or just before the position, and anything that can begin a value means it wanted a separator and did not get one, so insert at the position.

The separator errors, and how to tell them apart

A trailing comma before a closer

The most common JSON mistake there is, and the cause is muscle memory: a trailing comma is legal in a JavaScript object literal, in Python's dict syntax, and in JSON5 and JSONC, so a linter leaves one alone while the parser downstream refuses the file. The multi-line case is worse, because the message points at the closing line and the comma is at the end of the line above.

{
  "rows": 12,
}

Expecting property name enclosed in double quotes: line 3 column 1 (char 16)

A missing comma between two values

Between two multi-line members the message points at the start of the member that follows, and both lines look right in isolation. The position breaks the tie: read the line before it and check whether it ends in a comma or a closing bracket. The same fault inside an array gets a different sentence for the same repair:

[1 2]

Expecting ',' delimiter: line 1 column 4 (char 3)

A bracket that closes the wrong thing

An object whose array is closed by a brace:

{"tags": ["a", "b"}

Expecting ',' delimiter: line 1 column 19 (char 18)

Nothing is missing from that document that a comma would fix. Character 18 is the brace, which has arrived where a square bracket belongs, so the edit is to insert ] in front of it. The message asks for a delimiter because that is the parser's word for two values needing a separator, and it uses that word whether the missing something is a comma or a bracket.

So when a message asking for a comma does not go away after you supply one, stop supplying commas and count brackets: add one for every { and [, subtract one for every } and ], and find the first character where the total goes negative or a closer does not match its opener. That is the one diagnostic here that starts at the top of the file, and it is also how you find a closer missing from the very end.

The token errors, where the position is exact

A string in single quotes

{'name': 'Ada'}

Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

Character 1 is the single quote, and the fix is to retype it as a double quote. JSON has one string delimiter and no escape for the other one, so a double quote inside a string is written \" — which, in a source file holding that JSON, needs escaping again. That second round of escaping is why documents arrive with single quotes in the first place.

A key with no quotes

{name: "Ada"}

Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

Identical message, identical line, column and offset. One document has a single quote where a double quote belongs; the other has a letter where a quote belongs. The only thing separating them is the character at char 1. If you have ever applied what the message seemed to want and watched the error fail to move, this is the trap.

NaN, Infinity and a number ending in a dot

JSON's number grammar has no NaN, no Infinity, no leading plus sign, no leading zero, and no decimal point without a digit on both sides.

{"ratio": NaN}
Unexpected token 'N' in JSON at position 10

{"ratio": .5}
Unexpected token '.' in JSON at position 10

{"ratio": 5.}
Unexpected token '.' in JSON at position 11

All three positions are exact: character 10 is the N of NaN and the leading dot, and character 11 is the trailing dot. The repairs are null for a value that is genuinely absent, the string "NaN" for one that has to survive a round trip, 0.5 and 5.0.

Infinity and NaN are worth their own warning, because of where they come from: Python's json.dumps writes both by default and Python's own json.loads reads them back, so a pipeline can run for months and break the first time a JavaScript client reads the same file. The bytes were never valid JSON; one parser was lenient.

The characters you cannot see on screen

A byte-order mark is three bytes, EF BB BF, at the head of the file, decoding to one character, U+FEFF, at offset zero. Editors that default to UTF-8 with signature add it and then hide it, so the file looks correct in the window that produced it.

Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)

V8 quotes a character that renders as nothing: Unexpected token '\uFEFF' in JSON at position 0. Strip the leading character, save without the signature, or read with the BOM-aware decoder your language already ships. The second-order effect is the one that wastes time, because a BOM is one character and three bytes: every offset in the message is one further along than the text your editor shows you.

A comment is the next character that is visible to you and not to the grammar:

{ // rows still importing
  "rows": 12
}

Unexpected token '/' in JSON at position 2

JSON has no comment syntax. If the file is a configuration file, whether comments are allowed is decided by whatever reads it — JSONC and JSON5 are dialects rather than JSON, so the file fails at the first strict parser. The portable workaround is a key named "_comment".

The next two are inside strings, where the grammar is stricter than anywhere else.

{"note": "line one
line two"}

Bad control character in string literal in JSON at position 18

Character 18 is the line break itself. A JSON string may contain any character except an unescaped double quote, a backslash, and the control characters below U+0020, and a line break is one of those, so the parser rejects the break and not the string around it. A tab pasted in from a spreadsheet gives the same message. Escape the character at that offset — \n or \t.

{"path": "C:\Users\ada"}

Invalid \escape: line 1 column 13 (char 12)

Character 12 is the backslash before the U. The escapes the grammar has are \", \\, \/, \b, \f, \n, \r, \t, and \u with four hex digits. \U is not one of them, so the file must contain "C:\\Users\\ada". That is where escaping compounds: a source file holding that JSON needs each backslash escaped again, which is four characters in the source for two in the file.

Valid JSON that is still the wrong document

Not every failure here is a syntax error, and treating these as syntax errors is where the hours go, because the fix is not in the document. The tell is the position: the beginning of the file, the end of the file, or no parse error at all.

A truncated response. The parse fails at the end of the document rather than the middle of it: Unexpected end of JSON input, or Python's Expecting value: line 1 column 1 (char 0) when the body arrived empty. Both look like syntax errors, which is the trouble — no character is wrong, some are missing. Count brackets instead of rereading the text; if the first character's closer never arrives, nothing was mistyped, and the fault is outside the document.

The signature of that fault is a body starting with <, which gives Unexpected token '<' in JSON at position 0: an HTML error page from a proxy or a login redirect, where the zero is the parser saying the first character it saw was not the one you think you sent.

Two documents in one string. A log file or a streaming endpoint that writes one object per line is not a JSON document; it is several, and the format has a name — JSON Lines, or NDJSON. Hand the whole file to a parser and the first document parses while the rest is surplus:

{"a": 1}{"b": 2}

Extra data: line 1 column 9 (char 8)

Character 8 is the opening brace of the second document, which is the right place to point at, because everything before it was fine. Split on newlines and parse each line, or make the payload an array before it is written. A lenient mode for streams of concatenated values changes what "done" means: you no longer know whether the last object was the last object, or the last one before the connection dropped.

A string where you expected an object. This one produces no error at all, which makes it the most expensive of the group. Somewhere in the pipeline a payload was serialised twice, so the value you get back is a string whose contents are the JSON you wanted: "{\"items\": [1, 2]}". The parse succeeds, typeof data is "string", and data[0] is the opening brace rather than the first record.

if (typeof data === 'string') data = JSON.parse(data);

The guard matters as much as the parse: a string that genuinely is a string, and happens to contain braces, is not double-encoded, and parsing it again turns a correct document into a crash.

Valid JSON, wrong shape. The parse succeeds and the failure lands twenty lines later, where the code asks an object for something it does not have. No message will name a position, because the document is well formed and the disagreement is about what it means. The stack trace says which family you are in — a frame mentioning JSON.parse means syntax, and its absence means shape. The costliest version is an endpoint that returns an object for one record and an array for several: {"items": {...}} in one response, {"items": [{...}]} in the next.

The messages, what they mean, and the edit

The first column identifies the message, the second is what it almost always means, and the third is the edit. Where the position has a rule of its own, the rule is in the third column.

Message What it usually means The edit
Unexpected token '}'A trailing comma before a closer, or a closer of the wrong kindDelete the character before the position, or insert the closer that belongs there
Expecting property nameA trailing comma, a single-quoted key, or an unquoted keyA brace at the position means delete the comma before it; a quote means retype it
Expecting ',' delimiterTwo values with nothing between them, or the wrong bracket closing a valueInsert a comma at the position, or count brackets if the position is a closer
Expecting valueNothing where a value should beCheck whether the document ended early before you read the character
Unexpected token 'n'An unquoted keyQuote every key
Unexpected token 'N'NaN or InfinityNull, or the string "NaN" if the value has to round-trip
Unexpected token '.'A decimal point on the wrong side, as in .5 or 5.0.5 or 5.0
Bad control character in string literalA real line break or tab inside a stringEscape the character at the position as \n or \t
Invalid \escapeA backslash beginning no valid escape — usually a Windows pathDouble every literal backslash
Unexpected UTF-8 BOMA byte-order mark at offset zeroStrip it, or read with a BOM-aware decoder
Unexpected token '/'A commentDelete it, and check whether the reader accepts a dialect
Unexpected end of JSON inputA truncation or an empty bodyCheck the transport before the document
Unexpected token '<'An HTML error page where JSON was expectedRead the body before parsing it
Extra dataTwo documents concatenatedParse a line at a time, or wrap them in an array

The third column is a rule about the position, not about the text, because the text is the part the parser reuses. The bottom rows are the ones where the document is not the problem at all.

Where the tools fit. The JSON Formatter turns a position into a character faster than anything else I have here: paste the document, and it reports the line and column of the first syntax error and marks where the parser stopped. The tree view underneath is how I check that the shape I fixed matches the shape I meant.

When the trouble is shape rather than syntax — a list of records with a nested object inside each one — the JSON to CSV tool flattens it into a table, which is the quickest way to see that one record in four hundred has a structure the rest do not.

What I do before reading a single character

Take the offset from the message and print the character at it, with a little either side, escapes visible:

const i = 42;
console.log(JSON.stringify(doc.slice(Math.max(0, i - 20), i + 20)));

JSON.stringify is the part that matters. Printed raw, a byte-order mark, a tab and a line break all look like nothing, and a backslash doing the wrong job looks like a backslash. Passed through it, each one appears as its escape, and the invisible characters behind several of the failures above become visible in the one place they were hiding.

The habit I would most like to talk people out of is pasting the whole document into a validator and reading from the top. The parser did not read from the top and stop early; it read until it could not continue, and told you where that was. Start at the reported line, look at the character at the reported column, and read backwards to the previous complete value. If the answer is not within a line or so, the fault is a bracket, and the bracket count is the only diagnostic here that begins there.

The flat statement underneath all of it: the message names a character, never an intention. Expecting ',' delimiter is the parser's word for two values that must be separated by something, and it will print that word when the something missing is a square bracket. The category is vocabulary. The position is a measurement. Read the measurement first.