Plain JSON is minimal on purpose. The RFC 8259 grammar has no comments, no trailing commas and strict quoting rules. That works well for programs exchanging data. It is less pleasant for people editing config files, or for systems streaming millions of records. JSON variants exist to fill that gap.
This guide covers the three you will meet most: JSON5, JSONC and NDJSON. Each solves a different problem. JSON5 makes JSON easier to write by hand. JSONC adds only comments, for tooling config. NDJSON reshapes JSON for streaming and logs. You will see the syntax of each, when to use it, and what they have in common: none of them are valid standard JSON, so a normal parser rejects them.
Why JSON has so many variants
JSON became the default data format because it is small and strict. The spec fits on a few pages, every conforming parser agrees on what is valid, and there are no dialects. That strictness helps when programs exchange data. It gets in the way as soon as a person has to read or write JSON directly.
Three problems come up again and again. You cannot leave a comment that explains a value. A trailing comma after the last item is a syntax error, which makes diffs and reordering harder. Streaming a large array forces the parser to hold all of it in memory. Each variant below fixes one of these and stays close enough to JSON that most existing tooling still works.
| Variant | Adds | Primary use |
|---|---|---|
| JSON5 | Comments, trailing commas, unquoted keys, single quotes, more | Hand-edited config |
| JSONC | Comments (and trailing commas in practice) | Editor and tooling config (tsconfig, VS Code) |
| NDJSON | One JSON value per line | Logs, streaming, append-only data |
JSON5: a superset for people
JSON5 is the most permissive of the three. It is a strict superset of JSON, so every valid JSON document is also valid JSON5. It borrows syntax from ECMAScript 5 to make config easier to write. The main additions are comments, trailing commas, unquoted object keys and single-quoted strings. It also allows hexadecimal numbers, leading and trailing decimal points, explicit plus signs, Infinity and NaN, and multi-line strings with a backslash at the end of a line.
{
// Line comments and /* block comments */ are allowed
name: 'JsonKit', // unquoted key, single-quoted string
version: 2,
ratio: .5, // leading decimal point
flags: 0xFF, // hexadecimal number
offset: +1, // explicit plus sign
limit: Infinity, // not a valid JSON value
tags: [
'validate',
'format', // trailing comma is fine
],
}Since it is a superset, JSON5 needs its own parser. In JavaScript the reference implementation is the json5 npm package, which mirrors the built-in JSON API:
import JSON5 from "json5";
const config = JSON5.parse(`{
// trailing commas and comments survive the parse
host: 'localhost',
port: 8080,
}`);
console.log(config.port); // 8080
// JSON.stringify always emits strict, universally readable JSON
console.log(JSON.stringify(config)); // {"host":"localhost","port":8080}JSONC: JSON with comments
JSONC is a narrower idea: JSON plus comments, and nothing more. The name comes from Microsoft. It is the format behind some of the most edited config files around: tsconfig.json, VS Code's settings.json and .vscode/launch.json. Most JSONC parsers also accept trailing commas, but the defining feature is // line and /* block */ comments.
{
"compilerOptions": {
// Match the runtime your code ships to
"target": "ES2022",
"module": "ESNext",
"strict": true, /* enables every strict-mode check at once */
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}JSONC vs JSON5
The two overlap on comments and trailing commas, so people mix them up. JSONC stops there. Keys stay quoted and strings stay double-quoted, so it looks like ordinary JSON with notes. JSON5 goes further and allows unquoted keys, single quotes, hex numbers and Infinity. If you only need comments in a tooling config, JSONC keeps the file looking like JSON. Choose JSON5 when you want writing the file to feel like writing a JavaScript object literal.
Editors treat JSONC as its own language mode. To pass a JSONC file to a tool that expects strict JSON, strip the comments first. The JSON5 / JSONC to JSON converter removes comments and trailing commas, and the formatter below can then tidy the result.
NDJSON and JSON Lines: one value per line
NDJSON (Newline-Delimited JSON), also called JSON Lines or JSONL, solves a different problem from the other two. It does not relax the syntax. It changes the structure. Each line is a complete, independent JSON value, separated by a newline (\n). There is no enclosing array and no commas between records.
{"id":1,"event":"login","ts":"2026-07-12T10:00:00Z"}
{"id":2,"event":"click","ts":"2026-07-12T10:00:03Z"}
{"id":3,"event":"logout","ts":"2026-07-12T10:01:20Z"}This shape suits logs and data pipelines. You add a record by writing one more line, with no closing bracket to rewrite. A consumer can process the file line by line without loading all of it into memory, which matters for gigabytes of logs. One corrupt line does not break the whole file, so you can skip it and keep going. BigQuery imports, many logging systems and streaming APIs use it natively.
import fs from "node:fs";
import readline from "node:readline";
const rl = readline.createInterface({
input: fs.createReadStream("events.ndjson"),
crlfDelay: Infinity,
});
for await (const line of rl) {
if (!line.trim()) continue; // ignore blank lines
const record = JSON.parse(line); // each line IS valid JSON
console.log(record.event);
}To switch between a JSON array and NDJSON without code, use the JSON ⇄ JSONL converter.
Why standard parsers reject all of these
The key point: JSON5, JSONC and NDJSON are not JSON. The RFC 8259 grammar has no rule for comments, forbids trailing commas, requires double-quoted keys and strings, and treats a document as a single value. Give any of these variants to a compliant parser and it throws.
// JSONC / JSON5: a comment is a syntax error
JSON.parse('{ "a": 1 /* note */ }');
// SyntaxError: Expected ',' or '}' after property value in JSON at position 9
// JSON5: unquoted keys and single quotes are errors
JSON.parse("{ name: 'x' }");
// SyntaxError: Expected property name or '}' in JSON at position 2
// NDJSON: only the first value parses; the rest is trailing garbage
JSON.parse('{"id":1}\n{"id":2}');
// SyntaxError: Unexpected non-whitespace character after JSON at position 9This is not a bug to work around. It is the point of strict JSON. Interoperability depends on every parser agreeing on the grammar. The variants give up that agreement for easier editing or streaming, and expect you to use a matching parser (json5, jsonc-parser, a line reader) instead of the built-in one. SyntaxError wording differs across engines and versions, but the cause is the same. If you see an unexplained "Unexpected token" or "Expected" error, first check whether you are passing a variant to a strict parser. The guide to fixing JSON syntax errors covers the most common cases.
Choosing the right variant
Once you name the actual need, the choice is usually clear:
- You want comments in a config file that tooling reads (tsconfig, editor settings). Use JSONC. Those tools already expect it, and it looks like JSON.
- You want a hand-written config that feels like a JS object (unquoted keys, single quotes, hex). Use JSON5, and add a matching parser to your build.
- You are writing logs, event streams or append-only data. Use NDJSON, so records stream and append without rewriting the file.
- You are sending data over an API, storing a payload, or need the widest compatibility. Use strict JSON. It is the only form every consumer is sure to accept.
If comments are what you miss most and the file is config rather than data, consider YAML. It supports comments and is common for CI and infrastructure config. For anything sent over the network, convert your variant back to strict JSON and validate it first.
Working with variants in practice
A few habits keep variants from causing surprises. First, name files by their dialect. The extensions .json5, .jsonc and .ndjson (or .jsonl) tell editors, linters and teammates what to expect. Second, keep a clear boundary. Variants live in your source tree and tooling. Once data crosses a network or database boundary, it should be strict JSON. Third, automate the conversion so nobody forgets it.
Converting usually means parsing with the variant's parser, then writing with JSON.stringify. For NDJSON, that means joining minified lines into an array or splitting an array into lines.
// NDJSON string -> strict JSON array
function ndjsonToArray(text) {
return text
.split("\n")
.filter((line) => line.trim() !== "")
.map((line) => JSON.parse(line));
}
// strict JSON array -> NDJSON string
function arrayToNdjson(items) {
return items.map((item) => JSON.stringify(item)).join("\n");
}
const arr = ndjsonToArray('{"id":1}\n{"id":2}');
console.log(arr); // [ { id: 1 }, { id: 2 } ]
console.log(arrayToNdjson(arr)); // {"id":1}\n{"id":2}Once you have strict JSON, run it through a validator before shipping. The check takes seconds and catches a stray trailing comma or unquoted key carried over from the variant source.