Skip to content
jsonbeautifiers
English

The ten JSON mistakes that actually break payloads

Ranked by frequency, not by how interesting they are, with the exact error message each one produces.

Every claim here is either measured or sourced. Where it is neither, it says so.

Almost every “invalid JSON” ticket is one of ten things. The parser tells you a position, occasionally a character, and never the cause. Here they are in rough order of how often they show up, with the message you will have seen and the fix.

1. Trailing commas

The most common by a wide margin, because every other format you write all day allows them.

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

V8 gives you a message that mentions neither commas nor the thing you did:

Expected double-quoted property name in JSON at position 18 (line 1 column 19)

The parser consumed the comma, expected another key, and hit }. Now do the same thing in an array:

Unexpected token ']', "[1,2,]" is not valid JSON

Completely different wording for the identical mistake, because the array path in the parser fails on a different production. If you are searching an error string to work out what happened, that asymmetry is why you find nothing useful.

Python is more direct, but only recently. On 3.13 and later:

Illegal trailing comma before end of object

On 3.12 and earlier the same input gives Expecting property name enclosed in double quotes: line 1 column 19 (char 18). Same interpreter, same bug in your file, two different explanations depending on the runtime your CI happens to pin.

Fix: delete the comma. { "a": 1, "b": 2 }.

2. Single quotes

A Python dict that went through print() or str() instead of json.dumps():

{'ok': True}

That is not JSON and never was. It fails on the first quote:

Expected property name or '}' in JSON at position 1 (line 1 column 2)

Note True as well, which is a second, separate failure waiting behind the first. JSON booleans are lowercase.

{"ok": true}

Fix at the source: json.dumps(obj), and if the output is going into a UTF-8 file or an HTTP body, json.dumps(obj, ensure_ascii=False) so accented characters stay readable instead of becoming \uXXXX escapes. separators=(",", ":") if you want it compact.

3. Unquoted keys

A JavaScript object literal pasted straight into a JSON field:

{ name: "ada", active: true }
Expected property name or '}' in JSON at position 2 (line 1 column 3)

JSON requires every key to be a double-quoted string. Not single quoted, not bare, not a number. {"name": "ada", "active": true}. This is the same class of paste as the previous one and the fix is the same: get the value out of the runtime with a real serialiser rather than out of a console log.

4. Unescaped control characters

A real newline inside a string literal:

{"note": "line one
line two"}
Bad control character in string literal in JSON at position 18 (line 1 column 19)

Python calls it Invalid control character at: line 1 column 19 (char 18). Either way the parser is telling you that a character below U+0020 appeared inside a string where only its escape is allowed.

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

Tabs are the same problem and harder to see, because a tab pasted into a value looks like spaces. The root cause is nearly always JSON assembled with string concatenation, where a field containing a newline is dropped in verbatim. The last section deals with that properly.

5. Windows paths

{"path": "C:\Users\ada\config.json"}

\U and \a are not valid escapes. Python is explicit: Invalid \escape: line 1 column 13 (char 12). V8 says Bad escaped character in JSON at position 13 (line 1 column 14), pointing at the U rather than the backslash. The nine legal escapes are \" \\ \/ \b \f \n \r \t and \uXXXX. Everything else is an error, which is the correct design and constantly surprising.

{"path": "C:\\Users\\ada\\config.json"}

Forward slashes work fine on Windows in nearly every API, and they cost you no doubled backslashes. If you have a blob of text to embed and you would rather not do this by hand, the escape tool does it, and unescape goes the other way.

6. Invisible characters

This is the one that eats an afternoon. Two variants:

No-break space (U+00A0). Copy a snippet out of a documentation page, a chat client or a PDF and the spaces between tokens may not be spaces. RFC 8259 permits exactly four whitespace characters between tokens: space, tab, carriage return and line feed. U+00A0 is not one of them, so it is a syntax error, and it renders identically to the character next to it.

Curly quotes. Word and Google Docs autocorrect the straight quote U+0022 into the typographic pair U+201C and U+201D as you type. JSON accepts only U+0022. A document that looks perfectly quoted on screen has no string delimiters in it at all.

Neither variant produces a message that names the codepoint. Depending on where the character lands you get Expected double-quoted property name in JSON at position 8, or an Unexpected token ' ' that prints back a character you cannot tell from a normal space. Paste the document into the validator and it names the character and its codepoint at the exact offset, which is the fastest way to find it. JSON Repair strips them and tells you what it removed.

7. Comments

{
  // the user's display name
  "name": "ada"
}

V8 reports Expected property name or '}' in JSON at position 4 (line 2 column 3). Python stops with Expecting property name enclosed in double quotes: line 2 column 3 (char 4). Both point at the slash, and neither says the word comment, so the message reads as a quoting problem on a line that contains no strings.

JSON has no comment syntax. Crockford removed it deliberately, because people were using comments to carry parsing directives. If you control the consumer, JSONC (what VS Code uses for its own settings) allows comments and trailing commas, and JSON5 allows a great deal more. If you do not, move the prose into a field, or into the schema where descriptions belong. The full argument is worth ten minutes if you are picking a config format.

8. NaN and Infinity

{"ratio": NaN}
Unexpected token 'N', "{"ratio": NaN}" is not valid JSON

The trap is that Python emits this by default. json.dumps({"ratio": float("nan")}) produces {"ratio": NaN} and raises nothing, because CPython’s encoder is deliberately lenient and its own decoder accepts the value back. Every non-Python consumer rejects it.

json.dumps(obj, allow_nan=False)   # raises ValueError instead of shipping invalid JSON

Turn that on in your serialisation layer today. A NaN that reaches production is a division you did not guard, and you would rather find it at the encoder than in a client’s parser.

9. Duplicate keys

{"id": 1, "id": 2}

No error at all. RFC 8259 says keys SHOULD be unique and leaves the behaviour undefined when they are not. JavaScript and Python both keep the last one, so this parses to {"id": 2} and your first value is gone without a trace. Other parsers keep the first, and some raise. This is the only item on the list that is silent, which makes it the worst one. Run a payload through the validator, which flags duplicates rather than quietly collapsing them.

10. Numbers

Two failures share this slot.

Leading zeros. {"code": 007} is invalid. The JSON grammar allows a single 0 or a digit from 1 to 9 followed by more digits, and nothing else. A zip code, a country code or a part number with a leading zero is a string. {"code": "007"}.

Integers above 2^53-1. {"id": 12345678901234567890} parses fine and comes back as a different number, because JavaScript stores it as an IEEE 754 double and Number.MAX_SAFE_INTEGER is 9007199254740991. No error, no warning, wrong record. Send large IDs as strings; the long version covers why every fix other than that one is a workaround.

The structural fix

Half of this list (items 2, 3, 4 and 5) comes from the same habit: producing JSON with something other than a serialiser, usually string concatenation or a console log.

# every one of these is a bug waiting for the right input
body = '{"note": "' + note + '", "path": "' + path + '"}'

A newline in note breaks it. A backslash in path breaks it. A quote in either breaks it, and if that input came from a user it is an injection, not a formatting problem.

body = json.dumps({"note": note, "path": path}, allow_nan=False)

The serialiser escapes what needs escaping, quotes what needs quoting, and rejects what cannot be represented. It is not a style preference. Templating JSON by hand means reimplementing the escaping rules in section 7 of RFC 8259 correctly in every branch, and nobody does.

When you are handed a broken document rather than a broken producer, JSON Repair applies the fixes above and prints a list of every change it made, so you can tell whether it guessed at anything before you trust the output.