Read the error, then look one token earlier
JSON parse errors name the position where the parser gave up, which is usually one token after the mistake. A trailing comma is reported at the closing brace that follows it; an unclosed string is reported wherever the parser eventually gave up looking for the closing quote.
So the general method is: read the reported position, then look at the token immediately before it.
Paste your JSON and get the exact line →
The error table
| Error | Real cause | Fix |
|---|---|---|
Unexpected token } | Trailing comma after the last property | Delete the comma before } |
Unexpected token ] | Trailing comma after the last element | Delete the comma before ] |
Unexpected token ' | Single-quoted string or key | Use double quotes |
Unexpected token n (or another letter) | Unquoted key from a JS object literal | Quote the key |
Unexpected end of JSON input | Truncated document, or empty string | Check the input is complete |
Unexpected token < | The response is HTML, not JSON | Check the HTTP status and the URL |
Bad control character in string | Literal newline or tab inside a string | Escape as \n / \t |
Unexpected token at position 0 | Byte order mark before the first character | Re-save as UTF-8 without BOM |
Unexpected number | Leading zero, +, hex, or NaN | Use a plain decimal |
Unexpected token T | True/NULL instead of true/null | JSON literals are lowercase |
Duplicate key (strict parsers only) | The same key twice in one object | Remove the duplicate |
The five that account for most failures
1. Trailing comma
The most common JSON error, because modern JavaScript permits trailing commas and most linters actively encourage them.
{
"a": 1,
"b": 2,
}
The parser reads the comma after 2, expects a fifth token — another key — and finds }. Delete the comma.
Watch for this specifically in hand-edited config files, and in JSON assembled by string concatenation in a loop, where the separator is appended after every element including the last.
2. Single quotes
{'name': 'Ada'}
Valid JavaScript, invalid JSON. JSON permits double quotes only, for both keys and values. This nearly always comes from copying an object literal out of a .js file, or from Python's str(dict) — which produces single quotes and True/None. In Python, use json.dumps(obj), never str(obj).
3. Unquoted keys
{name: "Ada", age: 36}
Same root cause: JavaScript object literals allow bare identifiers as keys; JSON requires quoted strings. Every key needs double quotes.
4. HTML instead of JSON
Unexpected token < in JSON at position 0 means the body started with < — the beginning of <!DOCTYPE html>. The parser is working correctly; the request did not return JSON.
Usual causes: the endpoint 404'd and returned an error page, a session expired and a login page was returned, a proxy or CDN returned a notice, or the URL is simply wrong. Log the raw body and the status code before parsing:
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const ct = res.headers.get("content-type") ?? "";
if (!ct.includes("application/json")) {
throw new Error(`Expected JSON, got ${ct}: ${(await res.text()).slice(0, 200)}`);
}
return res.json();
5. The invisible ones
When JSON looks correct and still fails, the cause is usually a character you cannot see:
- Byte order mark (BOM) —
U+FEFFbefore the first{. Windows editors add it when saving as "UTF-8 with BOM". Re-save as UTF-8 without BOM. - Smart quotes —
"and"instead of". Introduced by Word, Google Docs, Notion, and chat clients. Never edit JSON in a word processor. - Non-breaking space —
U+00A0instead of a normal space, usually from copying out of a web page. - Literal newline inside a string — must be escaped as
\n.
A formatter reports the position of these even though your eyes cannot find them.
Language-specific gotchas
JavaScript. JSON.parse(undefined) throws "undefined" is not valid JSON — it stringifies the argument first. Guard before parsing. And JSON.stringify silently drops undefined values and functions from objects, so a round-trip is not always lossless.
Python. str(dict) produces {'a': True, 'b': None} — single quotes and capitalised literals, none of which is JSON. Always use json.dumps. Note also that Python's encoder writes NaN, Infinity, and -Infinity by default, which are not valid JSON; pass allow_nan=False to raise instead.
PHP. json_encode returns false on failure rather than throwing. Check json_last_error_msg(), or pass JSON_THROW_ON_ERROR.
Java / Jackson. Rejects unknown properties by default, producing UnrecognizedPropertyException on JSON that is perfectly valid but has extra fields. Configure FAIL_ON_UNKNOWN_PROPERTIES to false when consuming third-party APIs.
Debugging large files
Character offsets are useless in a 40,000-line file. Get a line number instead:
jq . large.json # reports line and column
python -m json.tool large.json # same, no dependency
# Newline-delimited JSON: find the offending line
jq -e . < data.ndjson > /dev/null || echo "invalid line above"
For files too large to inspect by eye, bisect: split the file in half and validate each part until you isolate the failing region.
Preventing errors instead of fixing them
- Generate JSON with a serialiser, never string concatenation. Every trailing-comma bug comes from building JSON by hand.
- Validate before writing — parse the string you just produced; if it throws, you caught it before it shipped.
- Check
content-typeand status before parsing a response, as above. - Use a schema for anything crossing a system boundary. JSON Validation covers JSON Schema.
- Format files on commit so malformed JSON never reaches the repository.
Related
- JSON Syntax Rules — the rules these errors violate
- JSON Validation — schema validation beyond syntax
- JSON Formatter — paste and get the line and column