Eternalsix

YAML and JSON hold the same data — but only YAML has these traps

YAML is JSON with nicer syntax, right up until a country code turns into false and a version number loses a digit. Here's every trap worth knowing, and when to use which.

JSON and YAML describe the same shapes: maps, lists, strings, numbers, booleans, null. YAML 1.2 was even designed so that every valid JSON document is also valid YAML. So teams reach for YAML when a config file starts collecting comments and nesting, and the migration feels free.

It isn't quite. YAML's convenience comes from guessing what you meant — and the guesses are where the bugs live. None of these are exotic; they show up in CI pipelines, Kubernetes manifests, and application config every day.

The one that bites: Norway is not a country, it's false

Write a list of country codes:

countries:
  - GB
  - FR
  - NO

In a YAML 1.1 parser — which includes PyYAML's default loader, still extremely common — NO is not the string "NO". It's the boolean false. So are no, No, off, Off, n, y, yes, and on. Your list becomes ["GB", "FR", False], and the failure surfaces somewhere far away, as a country lookup that returns nothing.

This is known as the Norway problem, and it is the single most common YAML surprise. YAML 1.2's core schema fixed it — only true and false are booleans — but plenty of tooling still parses as 1.1. You cannot assume which you have.

The fix is boring and reliable: quote strings that could be read as something else.

countries: ["GB", "FR", "NO"]

Numbers that quietly change value

Two more guesses in the same family.

Version numbers lose digits. version: 1.10 is a float, and 1.10 == 1.1. Your "version 1.10" becomes 1.1, which sorts before 1.9. Quote it: version: "1.10".

Leading zeros can mean octal. In YAML 1.1, 012 parses as 10, not 12. This bites hardest on things that look like numbers but aren't — ZIP codes, phone numbers, account IDs, git SHAs that happen to be all digits.

zip: 02134        # 1116 in some parsers, not "02134"
zip: "02134"      # what you meant

The general rule: if a value is an identifier rather than a quantity, quote it. You will never regret quoting a ZIP code. You will eventually regret not doing it.

Whitespace is syntax, and tabs are illegal

YAML uses indentation for structure, and it forbids tab characters for indentation entirely — not as a style preference, as a parse error. An editor configured to insert tabs produces a file that simply will not load, usually with an error pointing at a line that looks fine.

Indentation also has to be consistent within a block. Mixed two- and four-space nesting in the same map is a common cause of "why is this key not being read" — the parser understood it as a nested map somewhere you didn't intend.

Duplicate keys may not be an error

The YAML spec says duplicate keys in a map are invalid. Many parsers do not enforce it; they take the last one and say nothing. In a 300-line CI config where the same key appears at the top and again near the bottom, the top one silently does nothing. JSON parsers have the same weakness, but JSON files are usually machine-generated, while YAML files are hand-edited and long — which is exactly where a duplicate creeps in.

Multiline strings: | keeps newlines, > folds them

This one is less a trap than a thing people guess wrong. Both block scalars are useful, and they do different things:

literal: |
  line one
  line two
folded: >
  line one
  line two

literal is "line one\nline two\n". folded is "line one line two\n" — newlines become spaces. Use | for scripts, keys, and anything where line breaks matter. Use > for prose you're wrapping for readability.

The trailing newline is controlled by a chomping indicator: |- strips it, |+ keeps every trailing blank line. |- is what you want for a single-line secret or token that must not carry a \n.

Anchors save typing and can also be a denial of service

YAML lets you name a node and reuse it:

defaults: &defaults
  timeout: 30
  retries: 3

production:
  <<: *defaults
  timeout: 60

That's genuinely nice, and JSON has no equivalent. But because an alias can reference a node that itself contains aliases, a small file can expand to an enormous structure — the "billion laughs" attack. If you parse YAML that someone else supplies, use a parser with expansion limits.

Which leads to the bigger one.

Never use an unsafe loader on input you didn't write

Some YAML libraries can construct arbitrary language objects from tags like !!python/object/apply. Loading an untrusted file with the permissive loader is remote code execution, not a parsing bug. In PyYAML that means yaml.safe_load, never yaml.load without an explicit safe loader. Other ecosystems have the same split under different names.

JSON has no equivalent hazard, because JSON has no way to name a type. That alone is a good reason to prefer JSON for anything crossing a trust boundary.

When to use which

Use JSON for data that machines produce and consume: API payloads, build artifacts, anything serialized and parsed without a human in between. It has no comments and no guessing, which is exactly what you want from a wire format. It's also a subset of YAML 1.2, so a JSON file can be dropped into a YAML-reading tool as-is.

Use YAML for files humans edit and re-read: CI pipelines, deployment manifests, application config. Comments and multiline strings are worth real money in those files, and the traps above are all avoidable once you know them.

Don't convert back and forth casually. YAML → JSON is lossless in shape but drops comments, and they are often the most valuable part of the file. JSON → YAML is safe but will happily produce unquoted values that a later editor turns into the Norway problem.

Checking what a file actually parses to

The reliable way to settle "is this a string or a boolean" is to convert the file and look at the result — quotes in the JSON output tell you immediately how each value was read. A JSON ↔ YAML Converter that runs in your browser does this without pasting config into a third-party service, which matters when the file has credentials or internal hostnames in it. For the JSON side specifically, a JSON Formatter will also point at the exact character where a malformed file goes wrong.

The takeaway

YAML and JSON carry the same data; YAML adds comments, multiline strings, and anchors, and pays for them with type guessing. Quote anything that is an identifier rather than a quantity — country codes, versions, ZIP codes, git SHAs — and most YAML bugs disappear. Never load YAML you didn't write with a permissive loader, because that is code execution, not parsing. And when a value is behaving strangely, convert the file to JSON and look at whether it came out with quotes. That answers the question in one step.

For the neighbouring question of how text gets encoded for transport rather than structured, see Base64, URL encoding, and percent-encoding.