JSON vs YAML vs JSONC vs NDJSON — when to pick which
On this page
Four formats, all called “structured text”, all overlapping in obvious ways. Pick one for the wrong reason and you’ll fight tooling forever. Pick the right one and the tooling is silent.
This is a working engineer’s decision guide. Not a comprehensive spec — the specs are linked at the bottom. Just: when to pick which, what breaks if you don’t, and the surprises in each.
The contestants in one sentence each
- JSON — strict, machine-friendly, no comments, no trailing commas. The interchange format of the web.
- YAML — superset-ish of JSON; comments, multi-line strings, references. Whitespace-sensitive. Common for config.
- JSONC — JSON with comments and trailing commas. VS Code’s invention. Looks like JSON, parses with a JSONC parser.
- NDJSON — newline-delimited JSON. One JSON value per line. The format for streaming, log aggregation, and big datasets that don’t fit in memory.
The surprises start when you assume one is the others.
JSON — the boring default
Use JSON when:
- Two programs need to exchange structured data over the network.
- You want the broadest possible parser support (every language has a JSON parser in stdlib).
- You don’t need comments in the data itself.
Don’t use JSON when:
- Humans need to edit the file. JSON’s no-comments / no-trailing-commas rules make hand-editing fragile. Use JSONC or YAML.
- The data doesn’t fit in memory. JSON requires the whole document to parse before any value is accessible. Use NDJSON.
- You need exact-precision integers larger than 2^53. JavaScript’s
Numberloses precision past9007199254740992. The spec lets parsers represent numbers however they want, but most JS-based parsers silently round. If you’re sending big integer IDs, use strings.
The JSON spec is RFC 8259. It’s eight pages. Read it once.
A non-obvious part of “broad support”: field order is not preserved
across most parsers. A JSON object { "a": 1, "b": 2 } may come back
as { "b": 2, "a": 1 } after a round-trip. If order matters, encode
it explicitly — as an array.
Try JSON formatting / minifying / validating at json.tooljo.com.
YAML — comments, anchors, and the Norway problem
Use YAML when:
- A human edits the file regularly. CI configs, Kubernetes manifests, Docker Compose, Helm charts — these are YAML for a reason.
- You want comments alongside data.
- You want to deduplicate repeated structures via anchors (
&and*).
Don’t use YAML when:
- Two programs exchange data over the network. The implementation variance is enormous; YAML 1.1 vs 1.2 differ in surprising ways.
- The data has untrusted user input. YAML has historically had RCE
bugs (CVE-2017-2809 in
pyyaml’s default loader). Usesafe_load/ equivalent. Do not parse arbitrary YAML from the internet with default settings. - You need predictable behavior across languages. The Norway problem
is the canonical example: YAML 1.1 parses
NO(Norway’s country code) as the booleanfalse. So isY,N,yes,no,on,off. Quoting is the workaround:
country: "NO" # the string "NO"
country: NO # the boolean false (in YAML 1.1)
YAML 1.2 narrowed this — only true, false, True, False are
booleans — but most parsers still default to 1.1 mode. Always quote
two-letter values that might be confused with booleans.
The biggest YAML footgun: indentation. Tabs are not allowed. Spaces must be consistent within a block. A misplaced two-space indent silently turns a sibling key into a child. Invest in an editor that highlights indentation, or use a linter.
JSONC — JSON for humans
Use JSONC when:
- A config file is read by code but edited by humans. VS Code’s
settings.jsonis the original example. - You want comments and trailing commas without leaving the JSON-ish ecosystem.
Don’t use JSONC for interchange. JSONC is a strict superset of JSON,
but most JSON parsers reject the comments and trailing commas. The file
extension .jsonc signals to tooling that the file may contain those
features; .json does not. Mismatch the two and CI will fail at the
first comment.
The two extensions that make JSONC valid:
- Comments — both
// lineand/* block */. - Trailing commas —
[1, 2, 3,]and{"a": 1,}.
That’s it. No multi-line strings, no anchors, no references.
For tooling support: VS Code, Microsoft’s jsonc-parser library, and
Deno all parse JSONC out of the box. Most server-side languages need a
JSONC-aware library (in Python, json5
covers more than JSONC but works). Strip-then-parse is also reasonable
for one-off scripts:
// strip block + line comments and trailing commas, then JSON.parse
function loadJsonc(text) {
return JSON.parse(
text
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/\/\/.*$/gm, "")
.replace(/,(\s*[\]}])/g, "$1"),
);
}
(Don’t ship that in production — it breaks if a // appears inside a
string. Use a real JSONC parser. But for ops scripts, it’s enough.)
NDJSON — JSON that streams
Use NDJSON when:
- The dataset is too big to fit in memory.
- You’re streaming events, logs, or analytics.
- You want to append to a file without rewriting the whole thing.
The format is exactly what it sounds like: one valid JSON value per
line, separated by \n. No surrounding array, no commas between
records.
{"ts":1717000000,"level":"info","msg":"started"}
{"ts":1717000001,"level":"warn","msg":"retry","attempt":2}
{"ts":1717000002,"level":"error","msg":"failed","err":"timeout"}
This is the format Kubernetes uses for kubectl logs, what Loki
ingests, what most log shippers expect. It composes beautifully with
Unix tools: grep, jq, awk all work line-by-line.
The trap with NDJSON: don’t pretty-print individual records. The “one value per line” contract is broken if a single value spans multiple lines. Use a serializer that always emits compact JSON.
There’s a related-but-different format called JSON Lines
(.jsonl). It’s the same thing as NDJSON for practical purposes —
both are line-delimited JSON. Pick one extension and be consistent.
Cheat sheet — pick by use case
| Use case | Pick |
|---|---|
| HTTP API request/response body | JSON |
| CI/CD pipeline config | YAML |
| App config edited by humans | JSONC |
| Application logs / streaming events | NDJSON |
| Kubernetes / Helm / Docker Compose | YAML |
| Browser localStorage payloads | JSON |
| package.json, tsconfig.json | JSONC |
| Big dataset / analytics export | NDJSON |
| Inter-service RPC payload | JSON |
| GitHub Actions workflow | YAML |
Two surprises worth knowing
Indentation in YAML can change semantics. This works:
deploy:
steps:
- name: build
- name: test
This silently breaks:
deploy:
steps:
- name: build
- name: test
The second has 3-space indents. Some parsers accept it; some don’t. CI failures from “your YAML is valid on my machine” usually trace back to this. Use 2- or 4-space indents, never odd numbers.
JSON’s null and JavaScript’s undefined are not the same.
JSON has no undefined. JSON.stringify({ a: undefined }) produces
"{}", not '{"a":null}'. If you need explicit nulls — say, to clear
a database field — write the null yourself.
Further reading
- RFC 8259 — JSON spec
- YAML 1.2 spec
- JSONC reference
- NDJSON.org
For interactive JSON conversion (formatter, minifier, JSON ↔ YAML), see json.tooljo.com.