Two allowances, one name
“JSON with comments” is not a relaxed version of JSON. It is a different format that happens to share most of the grammar, and strict parsers do not know it exists. The two things people add almost always are comments and a trailing comma — and both produce messages that describe a different problem than the one you have.
A trailing comma is the clearest example. The comma itself is legal where it appears; what it promises is a member that never arrives. So the parser complains about the closing brace, one character later, and it complains in terms of what it expected rather than what it found.
| Input | Message | What was added |
|---|---|---|
| {"a":1,} | Expected double-quoted property name in JSON at position 7 (line 1 column 8) | A trailing comma in an object |
| [1,2,] | Unexpected token ']', "[1,2,]" is not valid JSON | The same comma in an array, reported without a position |
| {"a":1 // note} | Expected ',' or '}' after property value in JSON at position 7 (line 1 column 8) | A line comment after a value |
| {/* note */"a":1} | Expected property name or '}' in JSON at position 1 (line 1 column 2) | A block comment before the first key |
| {'a':1} | Expected property name or '}' in JSON at position 1 (line 1 column 2) | Single-quoted key, identical message to an unquoted one |
| {a:1} | Expected property name or '}' in JSON at position 1 (line 1 column 2) | An unquoted key |
| {"a":0x1F} | Expected ',' or '}' after property value in JSON at position 6 (line 1 column 7) | A hexadecimal number |
| {"a":.5} | Unexpected token '.', "{"a":.5}" is not valid JSON | A leading decimal point |
| {"a":+1} | Unexpected token '+', "{"a":+1}" is not valid JSON | An explicit plus sign |
| {"a":Infinity} | Unexpected token 'I', "{"a":Infinity}" is not valid JSON | A bare word where a value belongs |
| {"a":"one two"} | Bad control character in string literal in JSON at position 11 (line 1 column 12) | A literal newline inside a string, instead of \n |
Captured on 29 October 2026 with Node 22.22.2 (V8). Firefox and Safari phrase these differently but stop at the same character.
Where the file came from
Almost nobody types a trailing comma into JSON on purpose. The file is usually one of these:
- A configuration file. tsconfig.json, VS Code's settings.json, launch.json and devcontainer.json are all read by JSONC parsers, so every editor hint says comments are fine. They are — until the file is read by something else.
- A JavaScript object. A literal copied out of a .js file keeps its unquoted keys, single quotes and trailing commas. Every one of those is legal JavaScript and none is legal JSON.
- A printed object. Python prints dicts with single quotes; Ruby prints hashes with
=>. Neither output is JSON, whatever it looks like at a glance. - A hand-edited API payload. Adding a field by hand at the end of an object is where the comma goes in, and where it stays after the field is deleted again.
- A file saved by a Windows editor. Beyond the comma, it may have written a byte-order mark, which produces a message about an invisible character at position 0.
Four dialects, one of which is JSON
| Format | Specification | Allows beyond JSON | Typically found in |
|---|---|---|---|
| JSON | RFC 8259 | Nothing beyond the grammar | APIs, package.json, most config files |
| JSONC | No specification | Comments and trailing commas | tsconfig.json, VS Code settings.json and launch.json |
| JSON5 | Its own spec, json5.org | Comments, trailing commas, unquoted keys, single quotes, hex numbers, leading and trailing decimal points, Infinity, NaN | A few build tools, some embedded configuration |
| NDJSON / JSON Lines | Its own convention | Nothing new — but one complete value per line | Logs, data exports, stream processing |
The practical rule: the format is decided by the reader, not by the file extension. A file named config.json read by a JSONC parser may contain comments; the same bytes sent to an API will fail. That is why “it works in my editor” is not evidence.
Numbers have rules too
JSON5 accepts several number forms that JSON does not, and each one produces its own message. Some of the messages name the construct instead of the character, which makes them easier to act on:
| Input | Message | Why |
|---|---|---|
| 01 | Unexpected number in JSON at position 1 (line 1 column 2) | Leading zeros are not allowed. |
| 1. | Unterminated fractional number in JSON at position 2 (line 1 column 3) | A decimal point needs at least one digit after it. |
| .5 | Unexpected token '.', ".5" is not valid JSON | A number must start with a digit or a minus sign. |
| +1 | Unexpected token '+', "+1" is not valid JSON | A plus sign is not part of the grammar. |
| 1e | Exponent part is missing a number in JSON at position 2 (line 1 column 3) | The exponent marker needs digits. |
| - | No number after minus sign in JSON at position 1 (line 1 column 2) | A minus sign alone is not a number. |
| 1e400 | No error — it parses to Infinity | Valid by the grammar, unrepresentable as a double. Printed back, it becomes null. |
The last row is the one worth remembering. 1e400 is grammatically valid JSON and it parses without complaint — to Infinity, because no double can hold it. Serialise that value again and it becomes null, silently. A number that passes validation can still be destroyed by the round trip, which is the subject of the precision guide.
How to fix it without hand-editing
Deleting comments by hand works for one file and fails for a build pipeline. The durable version is to parse with something that understands the dialect, then re-serialise as strict JSON:
import JSON5 from "json5";
import { writeFileSync } from "node:fs";
const source = readFileSync("tsconfig.json", "utf8");
writeFileSync("tsconfig.strict.json", JSON.stringify(JSON5.parse(source), null, 2));For a file with only comments and trailing commas, a JSONC-aware reader such as jsonc-parser is the smaller dependency. Either way, the parse step is what makes it safe: it removes comments without touching the // inside "https://example.com", which any regular expression will happily eat.
If you only need to know why a file is rejected, paste it into the JSON Validator — it shows the reported position, the offending line and a plain-English reading of the message. To see what the file becomes once comments are gone, the formatter re-prints it, which makes the difference visible line by line.