A JSON config file that opens in VS Code is allowed to contain comments. A JSON file that opens in a strict parser is not. The same .json extension covers both, and the file looks identical until the parser refuses to load it. The reason is that at least five different things are now called "JSON," with overlapping syntax and incompatible promises.
The strict spec, RFC 8259 from December 2017, is one of them. JSON5, JSONC, JSON Lines, and NDJSON are the others. The differences are small enough that most developers learn them by surprise, on a Friday afternoon, when a build script chokes on a comma that worked fine in the editor.
There Are At Least Five Things Called "JSON"
The original specification is Douglas Crockford's, published on json.org in the early 2000s and standardized later through two parallel tracks. ECMA-404 (the Ecma International standard, first edition October 2013, second edition December 2017) and the IETF's RFC 8259 (December 2017, which obsoletes RFC 4627 from 2006 and RFC 7159 from 2013) define what most software means by "valid JSON" today. STD 90, the IETF's Internet Standard designation, points at RFC 8259.
The four common dialects layered on top:
- JSON5: A strict superset of JSON designed for human-edited configs. Allows comments, trailing commas, unquoted keys, single-quoted strings, hex literals, leading and trailing decimal points, and
NaN/Infinity. - JSONC: JSON with Comments, used internally by VS Code's
tsconfig.json,settings.json, and most editor config files. Adds line and block comments. Trailing commas are also tolerated in practice but not formally specified. - JSON Lines / JSONL: One JSON value per line, separated by
\n. Designed for streaming and log files. - NDJSON: Newline-Delimited JSON. Practically identical to JSON Lines; the spec at ndjson.org adds a few clarifications about line endings and partial-line writes.
Each dialect has a legitimate use case, and each one quietly assumes a different parser. Pasting a JSONC config into a strict JSON validator returns "unexpected token" on the first //.
The Strict Spec: RFC 8259 and What It Forbids
Strict JSON is more conservative than most developers remember. The grammar admits exactly six value types: object, array, string, number, true, false, null. It does not admit comments, trailing commas, single-quoted strings, unquoted keys, undefined, dates, regular expressions, or NaN/Infinity.
It does, since the 2014 revision, admit a top-level value of any type. RFC 4627 (the 2006 original) required the top-level value to be an object or array. RFC 7159 dropped that requirement, and RFC 8259 inherited the change. So 42 and "hello" are now valid JSON documents, where eight years earlier they were not. Plenty of older validators have not caught up.
The other corner the spec leaves ambiguous is number precision. Section 6 of RFC 8259 explicitly permits implementations to set their own range and precision limits, and notes that good interoperability comes from matching IEEE 754 binary64 (double precision). The grammar permits arbitrarily large integers; the recommendation pushes parsers toward 64-bit floats. That gap is where the 2^53 problem lives, and it surfaces below.
JSON5 and JSONC: The Human-Friendly Dialects
JSON5 grew out of frustration with strict JSON as a config format. The spec, currently at version 1.0.0 and maintained at json5.org, is explicit about being a superset: every valid JSON document is also valid JSON5, but not the other way around. JSON5 adds ECMAScript 5.1's object literal syntax, which gets you single-line // comments and block /* comments */, trailing commas in arrays and objects, unquoted keys when the key is a valid ECMAScript identifier, single-quoted or double-quoted strings, numbers with leading or trailing decimal points (.5 and 5.), hex literals (0xCAFE), Infinity, -Infinity, and NaN, plus line continuations inside strings.
JSONC is narrower. It only adds comments, and is mostly an editor convention rather than a published spec. VS Code's jsonc parser is the de facto reference, used in tsconfig.json, launch.json, settings.json, and the same Node-adjacent files that pretend to be strict JSON until someone slips in a comment. Strict JSON parsers will refuse them; JSONC parsers will silently strip the comments before parsing.
The takeaway is that "this file ends in .json" tells you very little about which parser will accept it. The directory the file lives in matters more than the extension.
NDJSON and JSON Lines: The Streaming Branch
JSON Lines and NDJSON are the same idea: separate each JSON value with a newline, write one value per line, never wrap them in an outer array. The format is designed for two cases.
The first is log streams, where you append events and a reader can tail the file or stream without waiting for a closing bracket. The second is large datasets that do not fit in memory as a single JSON document. A 50 GB file of one-object-per-line JSON can be processed line by line. A 50 GB JSON array cannot, unless your parser does incremental work.
Most data tools default to JSON Lines or NDJSON for export and ingest: BigQuery, OpenSearch, MongoDB's mongoimport, the OpenAI batch API, Hugging Face datasets, plenty of log shippers. The format is so well-supported that the strict JSON array form is now the odd one out for any data size larger than a few megabytes.
The trade-off is that a JSON Lines file is not itself valid JSON. The two formats share a value grammar; they do not share a document grammar. Tools that round-trip through JSON.parse on the whole file will choke on the first newline between objects.
The Dialect Comparison Table
The features that distinguish the five common dialects, laid out side by side:
| Feature | JSON (RFC 8259) | JSON5 | JSONC | JSON Lines / NDJSON |
|---|---|---|---|---|
// line comments | No | Yes | Yes | No (within values) |
/* block comments */ | No | Yes | Yes | No (within values) |
| Trailing commas | No | Yes | De facto yes | No |
| Unquoted keys | No | Yes | No | No |
| Single-quoted strings | No | Yes | No | No |
NaN / Infinity | No | Yes | No | No |
| Hex literals | No | Yes | No | No |
| Multiple top-level values | No | No | No | Yes (one per line) |
| Top-level non-container | Yes (since RFC 7159) | Yes | Yes | Yes |
| File extension convention | .json | .json5 | .json (in editor configs) | .jsonl / .ndjson |
There is no single dialect where everything else's syntax is legal. The closest is JSON5, which absorbs strict JSON and most of JSONC, but stops at the line-delimited structure.
The 2^53 Boundary: Where Even Strict JSON Breaks
The dialect that is hardest to use correctly is also the strictest. Strict JSON permits arbitrary-precision numbers in the grammar but invites parsers to use IEEE 754 binary64, and the most popular parser in the world, JavaScript's JSON.parse, takes the invitation.
JavaScript's Number type stores 53 bits of integer precision. Number.MAX_SAFE_INTEGER is 9007199254740991, which is 2^53 - 1. A 19-digit integer like 1234567890123456789, pasted as a JSON number into JSON.parse, comes back as 1234567890123456800. The transformation is silent. There is no warning, no exception, no flag set. The integer rounds to the nearest representable double, and the original is gone before any code sees it.
This is why Twitter's API began emitting id_str alongside id in 2011, after Snowflake-generated tweet IDs crossed 2^53. Discord did the same trick when their snowflakes overflowed. Stripe sidestepped it by making every API identifier a string from day one. RFC 7493 (March 2015) defines "I-JSON," a stricter profile that bans numbers outside IEEE 754 range entirely. The OpenAPI specification recommends I-JSON for new APIs, though adoption in practice is patchy.
The dialect choices for big-number safety are limited. Use strings for any 64-bit ID. Pick a parser that exposes the choice. Python's json returns arbitrary-precision ints natively. Go's encoding/json defaults to float64 but json.Number preserves the source. Rust's serde_json keeps integers in i64 by default and offers an arbitrary_precision feature for the rest. JSON5 inherits the boundary, JSON Lines inherits the boundary, JSONC inherits the boundary. None of the dialects solve the problem at the value layer; they all defer it to the parser.
If you want to see how your preferred formatter handles a 19-digit integer, paste one in. Jsonr is a browser-based formatter that uses JSON.parse for both prettify and minify, so its number behavior matches your runtime's. The rounding still happens. It just happens in your own tab, which is the difference worth caring about.