Skip to content
DevTinyTools

Syntax

Trailing commas, comments and single quotes: why your JSON5 is not JSON

The file came from a configuration editor that accepts comments, or from a console that printed a JavaScript object, or from a colleague who writes JSON5 without knowing it has a name. A strict parser accepts none of it, and its complaints are easy to misread.

8 min readTool: JSON Validator

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.

Verbatim parser messages for JSON5 and JSONC syntax
InputMessageWhat 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 JSONThe 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 JSONA leading decimal point
{"a":+1}Unexpected token '+', "{"a":+1}" is not valid JSONAn explicit plus sign
{"a":Infinity}Unexpected token 'I', "{"a":Infinity}" is not valid JSONA 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

What each JSON dialect allows
FormatSpecificationAllows beyond JSONTypically found in
JSONRFC 8259Nothing beyond the grammarAPIs, package.json, most config files
JSONCNo specificationComments and trailing commastsconfig.json, VS Code settings.json and launch.json
JSON5Its own spec, json5.orgComments, trailing commas, unquoted keys, single quotes, hex numbers, leading and trailing decimal points, Infinity, NaNA few build tools, some embedded configuration
NDJSON / JSON LinesIts own conventionNothing new — but one complete value per lineLogs, 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:

Number forms rejected by a strict JSON parser
InputMessageWhy
01Unexpected 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.
.5Unexpected token '.', ".5" is not valid JSONA number must start with a digit or a minus sign.
+1Unexpected token '+', "+1" is not valid JSONA plus sign is not part of the grammar.
1eExponent 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.
1e400No error — it parses to InfinityValid 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.

Frequently asked questions

Why does it work in my editor but not in the build?
Because the editor is not parsing JSON. VS Code reads settings.json, launch.json and tsconfig.json with a JSONC parser that tolerates comments and trailing commas, and TypeScript has its own tolerant parser for tsconfig.json. The moment the file is read by JSON.parse or by a strict server-side loader, those two allowances become errors.
Is JSON5 the same as JSONC?
No. JSONC adds comments and trailing commas and nothing else — it is the format Microsoft tooling uses. JSON5 goes further: unquoted keys, single quotes, hexadecimal numbers, leading decimal points, explicit plus signs, Infinity, NaN and multi-line strings. A file that is valid JSON5 is often not valid JSONC, and the reverse is always true.
Can I strip comments with a regular expression?
Not safely. A // inside a string value is a URL, not a comment, and a /* inside a string is text. A regex cannot tell which is which without parsing. Parse the document with a JSON5-aware parser and re-serialise it, which removes comments and normalises everything else at the same time.
What about a file that starts with an invisible character?
That is a byte-order mark, U+FEFF, added by some Windows editors. It is not whitespace to a JSON parser, so the message quotes what looks like an empty string. Strip the first character if it is U+FEFF before parsing; no format accepts it.
Why does the trailing comma error point at the closing brace?
Because a comma promises another member. The parser reads the comma, expects a key, and finds the closing brace instead — so it reports the position of the brace, not of the comma, and also reports a comma as expected rather than as unwanted. Read one character to the left of the reported position and the mistake is usually there.