Blog Productivity Tools Debugging Broken JSON: The Err...
Debugging Broken JSON: The Errors That Actually Happen
Productivity Tools Aug 04, 2026 10 min read 13 views

Debugging Broken JSON: The Errors That Actually Happen

A trailing comma on line 3 gets reported as an error on line 4. Here is what each JSON parse error actually means, which mistakes produce identical messages, and the two failures that never raise an error at all.

D
Daniel
Author

A trailing comma on line 3 gets reported as an error on line 4.

That trips up more people than anything else JSON does, and it is not a bug. Once you know what the parser is telling you, most of these errors stop being a hunt and become a lookup.

I collected the actual output of three parsers on the same set of broken documents, because the messages quoted online are often from old engine versions and no longer match what you see. Everything below is real output from Node 22.22.0 running V8, Python 3.11.9, and PHP 8.4.20.

Two of the failures in this article never produce an error at all. Those are the ones that cost real money.

Why the line number in the error is usually wrong

Take this file.

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

V8 says: Expected double-quoted property name in JSON at position 22 (line 4 column 1).

Line 4 is the closing brace. There is nothing wrong with line 4. The mistake is the comma at the end of line 3.

The reason generalises to every parse error you will read. A comma inside an object is a separator, so it promises another property name. The parser accepts it, skips the whitespace and the newline, and arrives at }. That is the first character that cannot legally follow what it just read, so that is where it stops.

Parsers report where they gave up, not where you slipped. When a reported line looks perfectly fine, look at the end of the line above it. That one habit resolves most trailing comma reports in seconds.

Python phrases the same failure as Expecting property name enclosed in double quotes: line 3 column 1 (char 12). Same cause, different arithmetic, and note it disagrees with V8 about which line to blame.

A trailing comma in an array behaves differently again. [1, 2, 3,] gives Unexpected token ']' with a snippet of your document and no position number whatsoever. Same class of mistake, two entirely different messages, depending on whether you were inside an array or an object.

HTML source code open in a text editor

Four different mistakes, one identical error message

This one genuinely surprised me. All four of these documents produce the exact same V8 error, character for character: Expected property name or '}' in JSON at position 4 (line 2 column 3).

What you wrote Why it fails
{ 'name': "Ali" } Single quotes. JSON strings are double-quoted only.
{ name: "Ali" } Unquoted key. Legal in JavaScript, not in JSON.
{ “name”: "Ali" } Smart quotes, usually from a word processor.
{ // note then "a": 1 } A comment. JSON has no comments.

Position 4 is where a property name was supposed to start. The parser does not care which wrong thing it found there, only that it was not a double quote or a closing brace. So the error tells you the location and nothing at all about the cause.

The smart quote case is the cruel one, because at normal font sizes “name” and "name" are nearly indistinguishable. If a snippet came out of an email, a chat client, a Word document or a CMS text field, assume the quotes were rewritten until you have proved otherwise.

Unexpected token 'N' means somebody handed you NaN

Three messages that look cryptic and mean something very specific:

Message What is actually in the file Where it came from
Unexpected token 'N' NaN Python, or a serialiser handling a failed calculation
Unexpected token 'u' undefined JavaScript, hand-edited or badly templated
Unexpected token 'T' True Python, printed rather than serialised

The first is the one you will meet in production. RFC 8259 is explicit that numeric values which cannot be represented in the grammar, such as Infinity and NaN, are not permitted. Python's standard library ignores that by default. Ask it to serialise a float nan and it writes {"a": NaN} quite happily. Infinity comes out as {"a": Infinity}.

So a Python service divides by zero somewhere, writes a document that no JavaScript client can read, and the failure surfaces in the browser rather than at the point where it was created.

The fix belongs on the producing side. Pass allow_nan=False to json.dumps and it raises ValueError: Out of range float values are not JSON compliant at the moment the bad value is written, which is where you want to find out. If you cannot change the producer, replace the value with null before it goes over the wire.

A stray Unexpected token 'T' almost always means a Python dict was interpolated into a string rather than serialised, so True and None went out where true and null belonged.

The character you cannot see

Feed V8 a file that starts with a UTF-8 byte order mark and it reports Unexpected token followed by a character that renders as nothing at all. You get an error naming something invisible, at position 0, in a document that looks correct in every editor you open it in.

RFC 8259 covers this in section 8.1. Implementations MUST NOT add a byte order mark to transmitted JSON, but parsers only MAY ignore one. That gap is why the same file works in one language and fails in another.

Python handles it best of the three, with Unexpected UTF-8 BOM (decode using utf-8-sig), which tells you both the problem and the fix. PHP, as we will see, tells you nothing.

You get one when a file is saved as "UTF-8 with BOM", which Notepad did by default for years and which Excel still adds on some exports. In VS Code the encoding sits in the status bar, and switching it to plain "UTF-8" and saving again clears it.

Bad control character in string literal

This message means a raw newline or tab is sitting inside a string, where only its escaped form is legal.

It comes from copying and pasting. Paste a two line address into a JSON string and you get Bad control character in string literal in JSON at position 12 (line 1 column 13). A raw tab does the same thing one character earlier in an equivalent document.

Both are legal once escaped, as \n and \t. The parser is not objecting to the character, only to it appearing literally. Anything you pasted from a spreadsheet cell, a log file or a terminal is a candidate.

Bad escaped character, or why Windows paths break JSON

{"path": "C:\Users"} fails with Bad escaped character in JSON at position 13 (line 1 column 14).

The backslash starts an escape sequence, and \U is not one JSON recognises. The complete list is \", \\, \/, \b, \f, \n, \r, \t and \uXXXX, and that last one is case sensitive, so a capital \U is invalid even before you count the hex digits. Write the path as C:\\Users and it parses to the single backslash you wanted.

I checked this twice, because my first test doubled the backslash before the parser ever saw it and appeared to show JSON silently swallowing the character. It does not. It errors. When testing escape behaviour, print the raw string first and confirm what you are actually handing the parser.

JavaScript in an editor with line numbers down the left side

Unexpected non-whitespace character after JSON

Two objects in one file, one after the other, produces Unexpected non-whitespace character after JSON at position 8 (line 2 column 1). Python calls the same thing Extra data: line 2 column 1 (char 8).

A JSON document contains exactly one value. The parser read your first object, found it complete and well formed, and then found more text after it.

Nine times in ten the file is JSON Lines, where each line is its own independent document. Log exports, model outputs and streaming APIs all use it. It is not broken JSON, it is a different format that needs reading line by line.

The errors nobody gets

Now the two that produce no error anywhere, which makes them considerably more dangerous than everything above.

Duplicate keys. Parse {"a":1,"a":2} in JavaScript, Python or PHP and all three give you a value of 2. No warning, in any of them. RFC 8259 says names within an object SHOULD be unique, and SHOULD is a recommendation, so parsers are free to accept duplicates. Last one wins. Merge two config files carelessly and a setting vanishes with nothing in the logs to say so.

Large integers. This one is worse, because the data changes rather than disappearing.

Parser 12345678901234567890 becomes
JavaScript 12345678901234567000
PHP 1.2345678901234567e+19
Python unchanged

JavaScript numbers are IEEE 754 doubles. Above 9007199254740991, which is Number.MAX_SAFE_INTEGER, consecutive integers stop being representable. Parse 9007199254740993 and you get 9007199254740992 back. The last digits of a long identifier are quietly replaced with zeros, no error is raised, and the record you go looking for afterwards does not exist.

RFC 8259 anticipated this. It guarantees that integers in the range from -(2**53)+1 to (2**53)-1 are interoperable, meaning implementations will agree exactly on their value, and offers no guarantee outside it. Snowflake IDs, Twitter-style IDs and most database bigints sit well outside that range. Send them as strings. Every API that has been burned by this already does.

What a JSON validator can tell you, and what it can't

A validator answers exactly one question: does this text parse. That is worth a great deal for every error above, and nothing whatsoever for the two silent failures.

The one I keep open is our own JSON validator that runs in the browser, for a specific reason. It calls JSON.parse in the page itself, with no upload and no server round trip, so I can paste a production payload in without thinking about where the data goes. It checks as you type rather than on a button press, and recomputes the line and column from the character offset in the engine's message.

What it will not tell you is that your duplicate key overwrote something, or that an ID lost its last three digits. Valid is not the same as correct. No validator can flag either of those, because both documents are entirely legal JSON.

Worth knowing: the message text comes from your browser's JavaScript engine, not from the page. Chrome, Firefox and Safari word the same failure differently, and V8's wording has changed across versions. The position is the dependable part.

If you are debugging in PHP, a browser validator is not a convenience, it is the only thing that gives you a location. json_last_error_msg() returned the identical string, Syntax error, for the trailing comma, the BOM, single quotes, the NaN and the truncated document. Five different faults, one message, no position, nothing to act on.

The 30 second checklist to fix JSON that will not parse

In the order I actually work through them.

  1. Read the reported position, then look at the line above it. Trailing commas account for more of these than everything else together.
  2. Error at position 0 on a file that looks fine? BOM. Re-save as UTF-8 without BOM.
  3. Unexpected token naming a single capital letter? A language leaked in. N is NaN, T is True, u is undefined.
  4. Position looks correct but the character looks fine? Smart quotes. Check anything that passed through a word processor or chat client.
  5. Bad control character? A raw newline or tab inside a string, from a paste.
  6. Bad escaped character? A Windows path. Double the backslashes.
  7. Complaint about content after a valid object? JSON Lines. Parse it a line at a time.
  8. It parses cleanly but the data is wrong? Check for duplicate keys, then check whether any ID exceeds 9007199254740991.

Step 8 is the one I would add to your review checklist if you take nothing else from this. The first seven cost you a few minutes each and announce themselves loudly. The eighth waits until the numbers stop reconciling.