The message says where, not what
A strict JSON parser reads left to right and stops at the first character it cannot accept. The text it throws back describes that moment: the token it found, sometimes the position, and a phrase like “Expected property name”. It never tells you which of the ten possible mistakes you actually made — that inference is still yours.
This matters because the reported position is usually a few characters after the real mistake. A missing comma is not noticed until the parser meets the next key. An unclosed brace is not noticed until the end of the file, so the error points at the last line instead of the line you forgot to close.
Seven messages you will actually see
These are not paraphrased. Each message below is what the engine returned for the input beside it, captured on Node 22.22.2 — the same V8 that ships in current Chrome and Edge.
| Input | Message | What it means |
|---|---|---|
| {"a":1,} | Expected double-quoted property name in JSON at position 7 (line 1 column 8) | Trailing comma. After the comma the parser still expects another member. |
| [1,2,] | Unexpected token ']', "[1,2,]" is not valid JSON | The same mistake in an array — and this time no position is reported at all. |
| {a:1} | Expected property name or '}' in JSON at position 1 (line 1 column 2) | Unquoted key. A single-quoted key produces exactly the same message. |
| {"a":1 // note} | Expected ',' or '}' after property value in JSON at position 7 (line 1 column 8) | The comment is seen as unexpected characters after a complete value. |
| {"a":1 | Expected ',' or '}' after property value in JSON at position 6 (line 1 column 7) | Reported at the last character, but the missing brace is the real problem. |
| {"a":1}{"b":2} | Unexpected non-whitespace character after JSON at position 7 (line 1 column 8) | Two top-level values. A JSON document holds exactly one. |
| {"a":NaN} | Unexpected token 'N', "{"a":NaN}" is not valid JSON | NaN is a JavaScript value, not a JSON one. |
Messages are not part of any specification, so an engine update can reword them. The position, when present, is the part that stays meaningful.
Reading position, line and column
The three numbers are not redundant. Position is a zero-based offset into the whole document, which is what you need when you are slicing the string programmatically. Line and column are one-based and exist so a human can find the spot in an editor.
Take {"a":1,}. The message reports position 7, line 1, column 8. Position 7 is the eighth character — the closing brace — and column 8 is the same character counted from one. When the input ends prematurely, as in {"a":1, the position equals the length of the string, which is the parser saying “I needed another character and there was none”.
When there is no location at all
Two of the seven rows above carry no position, only the document echoed back. That is not a bug and it is not rare: some failure paths produce a message shaped Unexpected token 'X', "…" is not valid JSON with the offending snippet inlined instead of a number.
The workaround is to re-read the document with the same grammar rules and report the first offset where reading cannot continue. That is what the JSON Validator does, and it labels those locations approximate — at the exact boundary character, two readings can both be defensible, so pretending to single-character precision would be dishonest.
The causes that cover most failures
Almost every “invalid JSON” you will meet is one of these. They are ordered by how often they appear in practice, not by how interesting they are.
- A trailing comma. Allowed in JavaScript, Python dicts and JSON5; forbidden in JSON. The error usually says a property name was expected.
- A comment.
//and/* */are not JSON. A config file that someone “cleaned up” with comments is the classic source. - Unquoted or single-quoted keys. Keys must be double-quoted strings.
{a:1}and{'a':1}fail identically. - An unclosed brace, bracket or quote. Reported at the end of the input, not where the opening character sits.
- A missing comma between members. Reported when the parser meets the next key, one position later than you would like.
- Two top-level values. A concatenated stream of NDJSON lines is not one JSON document. Parse it line by line instead.
- JavaScript literals that are not JSON.
NaN,Infinity,undefinedand0x10are all valid JavaScript and all invalid JSON. - A byte-order mark. The BOM is invisible, sits before the first
{, and produces an error at the very start of a document that otherwise looks perfect. Files exported from Excel are a common source. - Smart quotes. Text pasted from a word processor can carry
“ ”instead of". They look identical at a glance and are different bytes.
The wording is not portable
Every engine invents its own phrasing. The same trailing comma that V8 describes as an expected property name is reported differently by Python's json module, by Go's encoding/json and by Java's Jackson, and none of those strings are API-stable.
The practical consequence: never parse the message. If you need to react to a failure in code, catch the exception, treat it as “invalid”, and surface the message to a human. Writing a regular expression against “Unexpected token” will break on the next runtime upgrade — which is exactly why the validator here reports the message verbatim instead of mapping it to a friendlier string.