Plain JSON is deliberately minimal. The RFC 8259 grammar has no comments, no trailing commas, and strict quoting rules — great for machines exchanging data, less pleasant for humans editing config files or systems streaming millions of records. That gap is why a family of JSON variants exists.
This guide covers the three you will actually meet in the wild: JSON5, JSONC, and NDJSON. Each solves a different problem. JSON5 makes JSON comfortable to write by hand. JSONC adds just comments for tooling config. NDJSON reshapes JSON for streaming and logs. We will look at concrete syntax, when to reach for each, and the one thing they all share — none of them are valid standard JSON, so a normal parser will reject them.
Why JSON Has Spawned So Many Variants
JSON became the default data format precisely because it is small and rigid. The specification fits on a few pages, every conforming parser agrees on what is valid, and there are no dialects to negotiate. That rigidity is a feature for machine-to-machine exchange, but it creates friction the moment a human has to read or write JSON directly.
Three pain points come up again and again: you cannot leave a comment explaining why a value is set the way it is; a trailing comma after the last item is a syntax error that complicates diffs and reordering; and streaming a large array forces a parser to hold the whole thing in memory. The variants below each attack one of these problems while staying close enough to JSON that existing tooling mostly 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/tooling config (tsconfig, VS Code) |
| NDJSON | One JSON value per line | Logs, streaming, append-only data |
JSON5: A Human-Friendly Superset
JSON5 is the most permissive of the three. It is a strict superset of JSON — every valid JSON document is also valid JSON5 — that borrows syntax from ECMAScript 5 to make configuration comfortable to write. The headline additions are comments, trailing commas, unquoted object keys, and single-quoted strings, but there is more: hexadecimal numbers, leading and trailing decimal points, explicit plus signs, Infinity and NaN, and multi-line strings via backslash line continuation.
{
// 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
],
}Because 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, nothing more ambitious. The name comes from Microsoft, and it is the format behind some of the most-edited config files in the ecosystem — tsconfig.json, VS Code's settings.json, and .vscode/launch.json. In practice most JSONC parsers also tolerate trailing commas, but the defining feature is the ability to annotate config with // 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 they are easy to conflate. The distinction: JSONC stops there, keeping keys quoted and strings double-quoted, so it looks like ordinary JSON with notes. JSON5 goes further, allowing unquoted keys, single quotes, hex numbers, and Infinity. If you only need comments in a tooling config, JSONC keeps the file visually identical to JSON; JSON5 is for when you want the writing experience to feel like an ES object literal.
Editors handle JSONC by treating it as a distinct language mode. To feed a JSONC file to a tool that expects strict JSON, strip the comments first — a formatter can normalize it for you.
{ }JSON FormatterOpen the tool→NDJSON and JSON Lines: One Value Per Line
NDJSON (Newline-Delimited JSON), also known as JSON Lines or JSONL, solves an entirely different problem than the previous two. Instead of relaxing syntax, it changes 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 has real advantages for logs and data pipelines. You append a new record by writing one more line — no need to rewrite a closing bracket. A consumer can process the stream line by line without loading the whole file into memory, which matters when the file is gigabytes of log data. And a single corrupt line does not invalidate the entire document; you can skip it and keep going. It is the native format for tools like BigQuery imports, many logging systems, and streaming APIs.
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);
}Why Standard Parsers Reject All of These
The most important thing to internalize: JSON5, JSONC, and NDJSON are not JSON. The RFC 8259 grammar has no production for comments, forbids trailing commas, requires double-quoted keys and strings, and treats a document as a single value. Feed any of these variants to a compliant parser and it will throw.
// JSONC / JSON5 — comment is a syntax error
JSON.parse('{ "a": 1 /* note */ }');
// SyntaxError: Expected ',' or '}' after property value in JSON at position 9
// JSON5 — unquoted key 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 whole point of strict JSON. Interoperability depends on every parser agreeing on the grammar. The variants trade that universality for ergonomics or streaming, and they expect you to use a matching parser (json5, jsonc-parser, a line reader) rather than the built-in one. The exact wording of a SyntaxError differs across engines and versions, but the cause is the same. If you ever see a mysterious "Unexpected token" or "Expected" error, the first thing to check is whether you are handing a variant to a strict parser. Our guide to fixing JSON syntax errors walks through the most common cases.
Choosing the Right Variant
The decision is usually straightforward once you name the actual need:
- You want comments in a config file that tooling reads (tsconfig, editor settings): use JSONC. It is what those tools already expect and it stays visually close to JSON.
- You want a rich, 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 maximum compatibility: use strict JSON. No exceptions — this is the only form every consumer is guaranteed to accept.
If comments are the main thing you miss and the file is config rather than data, it is worth asking whether YAML fits better — it supports comments natively and is common for CI and infrastructure config. For everything on the wire, convert your variant back to canonical JSON and validate it before it leaves your machine.
✓JSON ValidatorOpen the tool→Working With Variants in Practice
A few habits keep variants from causing surprises. First, name files by their dialect — .json5, .jsonc, and .ndjson (or .jsonl) signal intent to editors, linters, and teammates. Second, keep a clear boundary: variants live in your source tree and tooling; the moment data crosses a network or database boundary, it should be strict JSON. Third, automate the conversion so it is not a manual step you forget.
Converting is usually a parse-then-restringify: read with the variant's parser, write 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 to confirm it parses cleanly before shipping — a thirty-second check that catches a stray trailing comma or an unquoted key that slipped through from the variant source.