JSON vs YAML: What’s the Difference and When Should You Use Each?
Most "JSON vs YAML" comparisons stop at "JSON has braces, YAML has indentation." That framing misses the part that actually pages engineers at 2 a.m. The two formats are not siblings — YAML is a strict superset of JSON, meaning any valid JSON document is also valid YAML 1.2. So the real question is never "can YAML represent this?" It is "will YAML represent it the way I meant, in every parser that reads it?" That is where the interesting failures live: YAML's convenience features silently change your data's type, and the same file can parse to different values in Python and JavaScript.
This guide skips the surface-level syntax tour and goes straight to the behavior that costs you time: the type-coercion traps, the whitespace rules, the reuse features that turn into denial-of-service vectors, and the one function call that has caused real remote-code-execution vulnerabilities. Then a clear verdict on which to reach for and when.
The short version
Use JSON for machine-to-machine interchange — API request and response bodies, message payloads, structured exports, anything two programs hand back and forth. It has no comments, strict grammar, and a parser in every language that behaves the same way everywhere. Use YAML for configuration that humans read and edit by hand — CI pipelines, Kubernetes manifests, application config — where comments and reduced punctuation are worth the price. And the price is real: surprising type coercion and whitespace sensitivity. Everything below is the reasoning behind that split.
YAML is a superset of JSON — literally
Because YAML 1.2 was redefined as a superset of JSON, you can paste raw JSON into a YAML parser and it will load without error. That is not a party trick; it is why so many tools accept "YAML or JSON" for the same config file — the YAML loader handles both. The flip side is that YAML adds a large amount of implicit behavior on top of JSON's explicit grammar, and that implicit behavior is the source of nearly every YAML surprise.
The type-coercion traps that cause real outages
JSON has exactly six value types and no guessing: if it is not in quotes and it is not a number,
true, false, or null, it is a syntax error. YAML instead
infers the type of every unquoted scalar. That inference is where the bugs breed.
The Norway problem
Under YAML 1.1 (still the spec PyYAML implements), the unquoted tokens yes, no,
on, off, true, and false — in several
case variations — are all parsed as booleans. Now imagine a list of country codes:
countries: - GB - US - NO - SE
NO is the ISO code for Norway. A YAML 1.1 parser reads it as the boolean
false. Your list of strings quietly becomes ["GB", "US", false, "SE"], and the
bug does not surface until some downstream code does country.upper() on a boolean and
throws. This is famous enough to have a name — the "Norway problem" — and the fix is trivial once you
know: quote the scalar, "NO".
Leading zeros, octal, and version strings
The same implicit typing mangles numbers. Under YAML 1.1, a bare 08 or 09 is
interpreted as octal — and since 8 and 9 are not valid octal digits, some parsers error and others do
something unexpected. A ZIP code or ID like 0755 can be read as the octal number 493. A
value like 1.10 loses its trailing zero and becomes the float 1.1, which
matters if it was a version string. The rule that saves you every time: quote anything that
is semantically a string but happens to look like a number — postal codes, phone numbers,
version strings, Git SHAs that start with a digit, MongoDB ObjectIds.
Same file, two answers: YAML 1.1 vs 1.2 across languages
Here is the trap almost nobody sees coming. YAML 1.2 dropped the yes/no/on/off
boolean aliases; only true/false remain booleans. But PyYAML, the
default parser in the Python ecosystem, still follows YAML 1.1, while many JavaScript loaders
target 1.2. That means one config file can parse to different values depending on which language reads
it:
enabled: no
In Python via PyYAML, enabled is the boolean False. In a 1.2-conformant
JavaScript loader, enabled is the string "no". If a Python service and a Node
service share the same config repo, they can disagree about whether a feature is on. No error is thrown
on either side — they just behave differently. Quoting the value (enabled: "no") removes
the ambiguity entirely.
Whitespace: tabs are illegal, and it is not negotiable
YAML forbids tab characters for indentation — spaces only. This is in the spec, not a linter preference. An editor that silently inserts a tab, or a copy-paste that carries one in, produces a document that fails to parse with an error that often points at the wrong line. JSON does not care about indentation at all; you can put an entire document on one line. If your team edits YAML by hand, configure the editor to expand tabs to spaces and turn on "render whitespace" before it costs you a debugging session.
Anchors, aliases, and the "billion laughs" attack
YAML has a feature JSON lacks entirely: anchors (&) and aliases (*) let you
define a node once and reference it elsewhere, so you can share a block of config without repeating it.
defaults: &defaults timeout: 30 retries: 3 production: <<: *defaults timeout: 60
Useful — but the same mechanism enables a denial-of-service payload known as the "billion laughs" or YAML bomb. By nesting aliases that each reference the previous level several times, a tiny file expands exponentially in memory when parsed, exhausting RAM and taking the process down. A document only a few lines long can demand gigabytes. If you parse YAML from untrusted sources, use a parser that caps alias expansion, and never load user-supplied YAML without limits.
The one function call that is a remote-code-execution hole
This is the single most important thing in this article. PyYAML's yaml.load(), when given
untrusted input, can construct arbitrary Python objects — which an attacker can turn
into remote code execution. A malicious document using YAML's language-specific tags can invoke
functions during parsing. The fix is not exotic: call yaml.safe_load() instead, which
restricts construction to simple, safe types. Modern PyYAML made load() without an explicit
Loader warn and behave more safely, but the habit that never bites you is: always
safe_load() anything you did not write yourself. JSON has no equivalent hazard —
JSON.parse and json.loads can only ever produce data, never behavior. That
asymmetry alone is a strong reason to keep untrusted interchange on JSON.
When to use JSON
Reach for JSON when the file is read and written by programs, not people, and when the data crosses a trust boundary:
- API request and response bodies, webhooks, and message-queue payloads
- Data you receive from clients or third parties (no coercion surprises, no code-execution risk)
- Structured exports and logs that other systems parse
- Anywhere you want byte-identical behavior across Python, JavaScript, Go, Rust, and the rest
JSON's lack of comments is occasionally annoying, but its lack of ambiguity is the whole point. What you wrote is exactly what every parser reads.
When to use YAML
Reach for YAML when a human maintains the file and the ergonomics matter more than strictness:
- CI/CD pipeline definitions and Kubernetes manifests, where comments explain why a value is set
- Application configuration edited by operators, where nesting stays readable without brace soup
- Infrastructure-as-code and tool config where a diff should read cleanly in code review
The comment support (#) is genuinely valuable — JSON has none — and the reduced punctuation
makes deep structures easier to scan. Just pay the tax knowingly: quote ambiguous scalars, use spaces,
and pin your parser's spec version if multiple languages read the file.
The same config, both ways
JSON
{
"app": {
"name": "ToolzYard",
"env": "production",
"region": "NO",
"version": "1.10",
"features": ["json", "yaml", "csv"]
}
}
YAML (written defensively)
# Region and version are quoted on purpose:
# NO would coerce to false, 1.10 would drop its zero.
app:
name: ToolzYard
env: production
region: "NO"
version: "1.10"
features:
- json
- yaml
- csv
Both encode identical data — but only because the YAML version defends against its own type inference. The JSON version needs no such care, because JSON never guesses.
Work with both formats
Convert between the two or validate a document before it ships:
Conclusion
JSON and YAML are not competitors so much as tools for different jobs. JSON's rigidity — no comments, no implicit typing, no code execution — is exactly what you want when programs exchange data across a network or a trust boundary. YAML's flexibility — comments, anchors, indentation — is exactly what you want when a person maintains a config file, provided you respect its quirks.
If you remember only three things: quote any string that looks like a number or a boolean, never call
yaml.load() on input you did not write, and remember that PyYAML (1.1) and many JS loaders
(1.2) can read the same file differently. Get those right and YAML's convenience stops being a liability.
Frequently Asked Questions
Is every JSON file also a valid YAML file?
Yes. YAML 1.2 is defined as a superset of JSON, so any valid JSON document parses cleanly in a YAML 1.2 loader. The reverse is not true — YAML's comments, anchors, and unquoted scalars have no JSON equivalent.
Why did my YAML value "NO" turn into false?
YAML 1.1 implicit typing treats unquoted no, yes, on, and
off as booleans, so the Norway country code NO becomes false.
Wrap it in quotes — "NO" — and it stays a string. The same fix protects version strings
and leading-zero IDs.
Can the same YAML file parse differently in Python and JavaScript?
Yes, and it is a real cross-language bug. PyYAML follows YAML 1.1, where enabled: no is
boolean false; many JavaScript loaders follow YAML 1.2, which dropped that alias and
reads it as the string "no". Quoting the value makes both agree.
Why should I avoid yaml.load() in Python?
PyYAML's yaml.load() on untrusted input can construct arbitrary Python objects, which
has led to remote-code-execution vulnerabilities. Use yaml.safe_load(), which only
produces plain data types. JSON parsers have no equivalent risk.
Why can tabs break a YAML file?
YAML forbids tab characters for indentation — the spec allows spaces only. An editor that inserts a tab produces a document that fails to parse, often with an error pointing at the wrong line. JSON is immune because it ignores indentation entirely.