ToolzYard Blog

Developer guides and tutorials

Format Comparison Guide

JSON vs YAML: What’s the Difference and When Should You Use Each?

Published: March 9, 2026 • Updated: July 10, 2026 • By , Founder of ThreeWorks

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.