JSON vs YAML: When Each One Actually Makes Sense
Arguments about JSON and YAML usually end where they should start: with the observation that they encode the same thing. Objects, arrays, strings, numbers, booleans, null. Anything you can express in one you can express in the other, and converting between them is mechanical enough that a converter in a browser tab handles it without asking you a single question.
So the choice is not about capability. It is about who is going to type into the file, who is going to read it under pressure, and what happens when one of them makes a mistake.
The same config, both ways
Here is a service definition in JSON:
{
"name": "billing-worker",
"replicas": 3,
"env": [
{ "name": "QUEUE_URL", "value": "https://queue.internal/billing" },
{ "name": "LOG_LEVEL", "value": "info" }
],
"retry": { "attempts": 5, "backoff": "exponential" }
}
And in YAML:
name: billing-worker
replicas: 3
env:
- name: QUEUE_URL
value: https://queue.internal/billing
- name: LOG_LEVEL
value: info
retry:
attempts: 5
backoff: exponential
The YAML is shorter and the nesting is carried by indentation rather than punctuation. For a file a human edits every week, that is a real ergonomic win. It is also, precisely, where YAML's failure modes come from.
What YAML buys you
Comments. This is the big one and it is not close. JSON has no comment syntax, which is why every JSON-based config format in wide use eventually grows a non-standard extension or a convention of dumping explanations into a _note key. If a human maintains the file, they need somewhere to write down why replicas is 3 and not 5.
Multi-line strings. A shell script or a certificate embedded in JSON becomes one long line studded with \n. YAML's block scalars keep it readable:
startup: |
set -euo pipefail
./migrate --wait
exec ./server
Less visual noise. No braces, no quotes on most values, no trailing-comma minefield. For deeply nested configuration this genuinely lowers reading effort.
What YAML costs you
The specification is enormous. JSON's grammar fits on a business card. YAML 1.2 is a document with anchors, aliases, tags, multiple document streams, and several string quoting modes with different escaping rules. Almost nobody uses more than a tenth of it, which means almost nobody recognises the other nine tenths when it shows up in a file.
Type inference surprises people. The classic is the Norway problem: NO is parsed as boolean false by YAML 1.1 parsers, so a list of country codes silently acquires a false in it. Version numbers are the other classic — version: 1.10 becomes the number 1.1, because trailing zeros are not a thing in floats.
Indentation is load-bearing. A tab where spaces were expected, or two spaces where four were expected, changes the shape of your data rather than failing loudly. JSON's braces are ugly precisely because they are explicit.
Anchors are a footgun at scale. &defaults and <<: *defaults look elegant in a tutorial and become very hard to trace in a 400-line file where the anchor is defined near the bottom.
What JSON buys you
One obvious parse. There is no inference. "1.10" is a string, 1.10 is a number, no is invalid unless quoted. When something is wrong, the parser stops and tells you where, which is why a formatter that validates as it indents is enough tooling for most JSON problems.
It is the wire format. Every HTTP API, every browser fetch, every logging pipeline already speaks it. Serialising to YAML to send over a network is work you are doing for no one's benefit.
Machines write it well. Generated files are JSON's home turf. Nobody needs comments in a lockfile.
The rule that actually holds up
Ask who types into the file.
- Humans type it, machines read it → YAML. Kubernetes manifests, CI pipelines, application config. The comments and the multi-line strings pay for the parser's quirks.
- Machines type it, machines read it → JSON. API payloads, lockfiles, build artefacts, anything generated. Nobody is reading it by hand, so terseness and comments buy nothing and predictability buys everything.
- Machines type it, humans read it → JSON, formatted. A minified API response is unreadable, but that is a display problem, not a format problem.
- Humans type it, humans read it, and it must not break → JSON with a schema, or a real programming language. If a config file is critical enough that a silent type coercion would be an incident, do not hand it to a format that guesses.
Two habits worth having
Quote anything that could be misread. Country codes, version strings, port numbers you intend as strings, anything starting with a zero. version: "1.10" costs two characters and removes a class of bug.
Validate before commit, not after deploy. The failure mode for both formats is the same: the file looks fine, the parser disagrees, and you find out in a pipeline. Run the file through a validator — if it round-trips to JSON and back with the values you expect, the types are what you think they are. That is a five-second check that catches the Norway problem and the version-number problem in one pass.
The formats are not rivals. They are the same data wearing clothes chosen for different occasions, and the only real mistake is dressing for the wrong one.