Both formats describe the same kinds of values: objects, arrays, strings, numbers, booleans, and null. Neither can express something the other can't, at the level of data. So the choice comes down to who reads and writes the file, and how much the format should forgive mistakes.
YAML has comments
JSON has no comment syntax. There's no official one and no convention that parsers reliably accept. Between programs that doesn't matter. In a config file that people edit by hand, it costs you. Being able to write # staging only, remove before prod right next to the value is the most common reason teams choose YAML for configuration. Config without explanations is how you end up asking "why is this set to this?" during an incident.
# YAML — comment lives right next to the value it explains timeout_ms: 3000 # bumped from 1000 after the 2026-09 incident // JSON has no equivalent — you're stuck with a separate README, or nothing
YAML depends on indentation
JSON marks nesting with braces and brackets, so a parser (and a person) can see where each block starts and ends no matter how it's spaced. YAML uses indentation as its only nesting signal. One misaligned space changes what the document means, not just how it looks. There's a separate guide on the error this produces, "All mapping items must start at the same column". It comes up a lot. Pasting a fragment into a spot with different nesting, or an editor's auto-indent adding a stray space, is enough.
The "Norway problem": implicit booleans
YAML 1.1, which many parsers have historically implemented (PyYAML's default loader and older Ruby implementations among them), turns certain unquoted scalars into booleans. no, off, and n become false. yes, on, and y become true. It's called the Norway problem because Norway's two-letter country code, NO, turns into false when it's left unquoted in a field that should hold a country code.
This has caused production bugs, but it depends on the spec version. YAML 1.2, which most newer parsers target (Go's yaml.v3, for example), cut the implicit booleans down to true and false and dropped the yes/no/on/off forms. What you get depends on your parser and the version it targets. Check it for any value that could be read as a keyword.
# Ambiguous under YAML 1.1 parsers — becomes the boolean false, not the string "NO" country_code: NO # Unambiguous under any YAML version country_code: "NO"
JSON is (nearly) a subset of YAML
Valid JSON is valid YAML 1.2. JSON's braces-and-brackets objects and arrays are part of YAML's "flow style" syntax, so a JSON document parses fine as YAML. It doesn't work the other way. Most YAML files use block style, comments, and anchors, none of which exist in JSON. Converting YAML to JSON is a real transformation, and the comments are lost because JSON has nowhere to put them. Converting JSON to YAML is closer to reformatting.
Anchors and aliases
YAML also has anchors (&name) and aliases (*name). You define a value once and reference it elsewhere in the same document. That helps in configs with a lot of repetition, like the same resource limits on several services. JSON has no way to say "this value is a reference to that one," so a converter has to resolve each alias and copy the value into the output. That's another reason YAML to JSON is more than a syntax swap.
# YAML anchors/aliases — no JSON equivalent defaults: &defaults timeout_ms: 3000 retries: 2 service_a: <<: *defaults name: service-a
Some YAML loaders can run code; JSON parsers can't
JSON's data model has no way to say "construct an instance of this type." Parsing JSON can only produce plain objects, arrays, strings, numbers, booleans, and null, so a JSON parser has no mechanism to run code while parsing. Some YAML implementations didn't have that guarantee.
Before PyYAML changed its defaults, yaml.load() used a loader that resolved Python-specific tags such as !!python/object/apply. That tag tells the loader to build an arbitrary Python object by calling a function, so loading an untrusted YAML string could execute arbitrary code. This is a well-documented class of vulnerability, tracked among others as CVE-2020-14343 against PyYAML. It's why PyYAML's docs have long pointed people to yaml.safe_load(), which only produces plain data types, for any YAML from an untrusted source. It's also why newer major versions made load() require an explicit loader argument instead of defaulting to the unsafe one.
The mechanism is specific to Python and PyYAML. The lesson carries over: find out whether your language's YAML library has a safe or restricted loading mode, and use it for anything that isn't a config file you wrote yourself.
Multi-document streams
One YAML file can hold several independent documents separated by a line with just ---. You'll see this all the time in Kubernetes, where one manifest file defines a Deployment and a Service together. JSON has nothing like it. A JSON text is exactly one value, so there's no standard way to say a file holds three separate documents without inventing a wrapper (an array of documents, or newline-delimited JSON). To convert a multi-document YAML stream to JSON, you have to pick one of those conventions yourself.
# One YAML file, two independent documents apiVersion: apps/v1 kind: Deployment metadata: name: web --- apiVersion: v1 kind: Service metadata: name: web
Schema validation: JSON Schema either way
JSON Schema is the most widely supported way to validate the shape of a JSON document. YAML files are usually validated the same way, with one extra step: parse the YAML, then hand the result to a JSON Schema validator as if it had been JSON all along. The schema language doesn't need a YAML variant, because by the time validation runs the comments, anchors, and indentation are gone and only the parsed values are left. Kubernetes Custom Resource Definitions work this way. They describe their expected shape with an embedded JSON Schema, even though the resources are nearly always written in YAML.
Which to use
For data that moves between programs (API responses, message payloads, anything parsed without a person reading the raw file), default to JSON. There are no comments to lose, no indentation to get wrong, and every language has a parser for its single grammar.
For config files people edit, such as application settings, CI pipelines, and infrastructure manifests, YAML's comments and lighter syntax are worth the indentation risk. That holds as long as the team quotes anything that could be read as a keyword and keeps indentation consistent. A linter that enforces both will catch the usual mistakes before they ship.