YAML vs JSON is a question almost everyone asks the first time they open a docker-compose.yaml, a GitHub Actions workflow or a Kubernetes manifest. Both formats store structured data as text, both know objects, lists, numbers and booleans. And yet in practice they behave completely differently.
The short answer: JSON is a strict, small data format that programs exchange with each other. YAML is a large, forgiving format meant to be written by humans. JSON is faster, unambiguous and safer. YAML is more readable, allows comments and saves you the brackets. The price of that convenience is that YAML guesses, and sometimes it guesses wrong.
So that this article does more than repeat textbook wisdom, on October 14, 2026 we pitted three YAML parsers against JSON on our own server: PyYAML 6.0.3 (Python), js-yaml 4.3.2 and yaml 2.9.1 (both on Node.js 22). Three results up front:
1.
JSON.parseread the same 50,000 records in 30 ms. js-yaml needed 308 ms, theyamllibrary 4.2 seconds, and pure-Python PyYAML 20 seconds.2. PyYAML read the country code
NOasfalse, the time22:22as the number 1342, and the postal code01067as 567. Without a single warning.3. A 254-byte YAML file turned into 22.9 MB of JSON once loaded. Only one of the three libraries refused it.

YAML vs JSON: The Fundamental Difference
If you’re not yet sure how a JSON file is structured, start with our primer What is a JSON file?. Here we focus on the head-to-head comparison.
What JSON looks like
JSON (JavaScript Object Notation) was popularized by Douglas Crockford in the early 2000s and is standardized today as RFC 8259. The entire grammar fits on a postcard: objects in curly braces, lists in square brackets, strings in double quotes, plus numbers, true, false and null. That’s it.
{
"server": {
"host": "example.com",
"port": 8080,
"debug": false
},
"countries": ["DE", "AT", "NO"]
}
What YAML looks like
YAML (recursively, “YAML Ain’t Markup Language”) appeared in 2001. The current version is YAML 1.2, last revised as 1.2.2 in 2021. YAML drops the brackets and most quotes; structure comes from indentation:
# Web server configuration
server:
host: example.com
port: 8080
debug: false
countries:
- DE
- AT
- "NO" # quotes required, see below for why
The example already shows YAML’s main strength: it reads almost like a note. And it shows its main trap, because the last line contains quotes that you’d naturally leave out if you didn’t know better.
YAML is (almost) a superset of JSON
Since YAML 1.2, every valid JSON document is also a valid YAML document. You can read a .json file with a YAML parser. We tested this: js-yaml read our 5.8 MB JSON test file without complaint, and actually faster than the same data in YAML block style (205 vs 308 ms). The reverse is not true: a JSON parser rejects almost every YAML file.
One caveat: this only holds cleanly for YAML 1.2 parsers. Older YAML 1.1 parsers, including the hugely popular PyYAML, trip over edge cases. More on that in a moment.
The differences in one table
| JSON | YAML | |
|---|---|---|
| Designed for | data exchange between programs | configuration written by humans |
| Structure via | brackets {} and [] | indentation (spaces only) |
| Comments | no | yes, with # |
| Quotes | required for strings and keys | mostly optional |
| Multi-line strings | only with \n | yes, with | and > |
| Reuse | no | anchors & and aliases * |
| Multiple documents per file | no (except NDJSON) | yes, separated by --- |
| Data types | 6, fixed | guessed from the value, version-dependent |
| Specification | a few pages | over 80 pages |
| Parse speed (our test) | 30 ms | 308 ms to 20 s |
| Duplicate keys | usually silent: last one wins | error or silent, depending on parser |
| Typical files | package.json, API responses, tsconfig.json | docker-compose.yaml, GitHub Actions, Kubernetes, Ansible |
Our Test Setup
All measurements ran on October 14, 2026 on our production server (Hetzner, Ubuntu, Node.js 22.23, Python 3). We tested three YAML libraries that you very likely have somewhere in your stack without knowing it:
- PyYAML 6.0.3: the Python default, implements YAML 1.1. It’s inside Ansible, many CI tools and countless scripts.
- js-yaml 4.3.2: the Node ecosystem default, implements YAML 1.2. With tens of millions of weekly downloads, one of the most-used packages in existence.
- yaml 2.9.1 (by Eemeli Aro): the more modern Node library, YAML 1.2 by default, optionally 1.1.
For the speed tests we generated 50,000 product records with a fixed random seed (ID, name, price, active flag, two tags, warehouse, quantity) and measured each run five times. The tables show the median.
Test 1: How Fast Is YAML Compared to JSON?

First, file sizes for identical data:
| File | Size | gzip -9 |
|---|---|---|
| JSON compact | 5.79 MB | 682 KB |
| JSON indented (2 spaces) | 11.29 MB | – |
| YAML (js-yaml output) | 7.29 MB | 707 KB |
YAML is about 26 percent larger than compact JSON but 35 percent smaller than indented JSON. Compressed, they’re practically identical. File size is rarely a real argument either way.
Parsing is a completely different story:
| Operation | Median | Factor vs JSON |
|---|---|---|
JSON.parse (Node) | 30 ms | 1× |
js-yaml load | 308 ms | ~10× |
js-yaml load on the JSON file | 205 ms | ~7× |
yaml 2.9 parse | 4,162 ms | ~140× |
Python json.loads | 92 ms | – |
PyYAML CSafeLoader (C extension) | 7,776 ms | ~85× vs Python JSON |
PyYAML safe_load (pure Python) | 20,388 ms | ~220× vs Python JSON |
And serializing:
| Operation | Median |
|---|---|
JSON.stringify | 18 ms |
yaml 2.9 stringify | 1,704 ms |
js-yaml dump | 3,680 ms |
What this means: for a 50-line config file it doesn’t matter at all; we’re talking microseconds. But the moment YAML is used as an exchange format for real data volumes, such as exports, caches or API payloads, it becomes the bottleneck. During the test series the Node process peaked at over 2 GB of memory, mostly due to the YAML libraries, which allocate far more bookkeeping per node than JSON.parse.
The gap between the two Node libraries is notable: yaml 2.9 parses more than ten times slower than js-yaml. That’s because yaml builds a full syntax tree including comments and positions, so you can edit a file and write it back without losing the comments. That’s a genuine feature, but it costs time.
For PyYAML, check your own installation: if yaml.CSafeLoader is available, it was 2.6 times faster than the default safe_load in our test. Many projects still don’t use it, because safe_load is the name every tutorial shows.
Test 2: The Norway Problem and Other Type Surprises

The most important difference between YAML and JSON isn’t syntax, it’s who decides the data type. In JSON the type is fixed: "NO" is a string, false is a boolean, done. In YAML the parser guesses the type from the value. And the rules for that changed between YAML 1.1 and 1.2.
We fed the same small file to all three parsers:
countries: [DE, AT, NO, SE]
active: yes
switch: on
permissions: 0755
old_octal: 010
time: 22:22
version: 1.10
zip: 01067
date: 2026-10-14
hex: 0x1F
The result, converted to JSON:
| Line | PyYAML 6.0.3 (YAML 1.1) | js-yaml 4.3.2 (1.2) | yaml 2.9.1 (1.2) |
|---|---|---|---|
NO | false | "NO" | "NO" |
yes | true | "yes" | "yes" |
on | true | "on" | "on" |
0755 | 493 (octal) | 755 | 755 |
010 | 8 | 10 | 10 |
22:22 | 1342 (sexagesimal) | "22:22" | "22:22" |
1.10 | 1.1 | 1.1 | 1.1 |
01067 | 567 (octal!) | 1067 | 1067 |
2026-10-14 | date | date (as timestamp) | "2026-10-14" |
0x1F | 31 | 31 | 31 |
This is the famous Norway problem: a list of country codes in which Norway is silently stored as false. It’s not hypothetical; it’s a classic from real bug reports.
A few lines deserve an explanation:
22:22becomes 1342. YAML 1.1 supports base-60 numbers, intended for times. 22 × 60 + 22 = 1342. If you write a time or a port mapping like22:22in a config, PyYAML hands you an integer.01067becomes 567. That’s the postal code of Dresden, Germany. YAML 1.1 reads leading zeros as octal. But the 1.2 parsers return 1067, which is also a number, just decimal. The leading zero is gone in all three cases. The same applies to US ZIP codes like02134.1.10becomes 1.1. In all three parsers. Write version numbers without quotes and you get version 1.1 instead of 1.10. This bites Python versions in CI files particularly often:python-version: 3.10becomes3.1.- The date is a midnight-UTC timestamp in js-yaml and a string in yaml 2.9. Same file, same YAML version, different types.
The lesson: in YAML, every string that could look like something else belongs in quotes. Country codes, postal codes, version numbers, times, phone numbers, yes/no values that are meant as text. In JSON, the question never comes up.
The GitHub Actions on: trap
GitHub Actions workflows provide a lovely example. Every workflow starts with:
on:
push:
branches: [main]
PyYAML reads the key as True, not "on". We measured it: {True: {'push': {'branches': ['main']}}}. GitHub itself uses a YAML 1.2 parser, so the workflow runs fine. But if you inspect or transform the file with a Python script, you’ll search in vain for the key on.
What about Docker Compose?
Docker Compose is lenient here. We checked a file with DEBUG: no and COUNTRY: NO. docker compose config (Docker 29.2.1, Compose v5.0.2) rendered both as the strings "no" and "NO". Compose uses a 1.2-style parser. The real traps in Compose files are covered in our Docker Compose example.
Test 3: Duplicate Keys
What happens when the same key appears twice? In long config files that happens more easily than you’d think, for instance after a copy-and-paste.
name: a
name: b
| Parser | Result |
|---|---|
| PyYAML | {'name': 'b'}, silent |
| js-yaml | error: duplicated mapping key (2:1) |
| yaml 2.9 | error: Map keys must be unique at line 2, column 1 |
JSON.parse (Node) | { name: 'b' }, silent |
Python json | {'name': 'b'}, silent |
jq | {"name": "b"}, silent |
Here YAML is, for once, the stricter format. The YAML spec explicitly forbids duplicate keys, and two of the three parsers enforce it. The JSON standard only says keys “should” be unique, and every JSON parser we tested quietly kept the last value. PyYAML behaves like a JSON parser and thereby violates its own specification.
Test 4: Indentation, Tabs and the Trailing Comma
YAML uses indentation as syntax. That produces three classes of error that don’t exist in JSON:
Tabs are forbidden. A file indented with a tab fails: found character '\t' that cannot start any token. That’s good, because the error is visible. The annoying part is that editors often don’t show tabs.
One space too many fails. Three spaces instead of two before host: produced an error at line 3, column 8. Also visible.
One space too few does not fail. This is the dangerous one:
server:
port: 80
host: x
Here host was accidentally left unindented. PyYAML happily returns {'server': {'port': 80}, 'host': 'x'}. The setting lands one level too high, the application can’t find server.host and falls back to the default. No error, just wrong behavior. In JSON the same shift would require a misplaced brace, which you’d spot immediately.
JSON’s strictness shows the other way around with the trailing comma: both JSON parsers reject {"a":1,}. Python says Expecting property name enclosed in double quotes: line 1 column 8, Node says Expected double-quoted property name in JSON at position 7. PyYAML reads the same line without complaint as {'a': 1}. If you want a format that forgives small sloppiness, YAML is your friend. If you want one that reports it, choose JSON.
Test 5: The 254-Byte Bomb (Anchors and Aliases)

YAML has a feature JSON lacks: anchors and aliases. &name marks a block, *name reuses it. That’s handy for avoiding repetition, for example shared settings across several services:
defaults: &defaults
restart: unless-stopped
logging: { driver: json-file }
web:
<<: *defaults
image: nginx
The same feature can be abused. You reference the previous level nine times on each level, and after seven levels you have 9^7 = 4,782,969 entries in memory. It’s the YAML version of the “Billion Laughs” attack known from XML. Our file was exactly 254 bytes:
a: &a ["x","x","x","x","x","x","x","x","x"]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c]
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d]
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e]
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f]
| Parser | Load | Then serialized to JSON |
|---|---|---|
| js-yaml | ok after 2 ms | 22,868,593 characters (22.9 MB) in 116 ms |
PyYAML safe_load | ok after 1 ms | 28.2 MB in 0.2 s |
| yaml 2.9 | rejected after 9 ms: Excessive alias count indicates a resource exhaustion attack | – |
Why was loading so fast? The parsers store aliases as references, not copies. In memory the document starts out small. The explosion happens the moment something walks the data: serializing, validating, writing to a database. One more level would have meant 43 million entries, two more 387 million, and then the server is gone.
JSON has no references. A 254-byte JSON file is roughly 254 bytes once parsed.
Test 6: When YAML Executes Code
The second security difference is more serious. YAML supports tags that mark a value as a specific type. PyYAML supports Python-specific tags that can call arbitrary functions while loading. Our test file:
x: !!python/object/apply:os.system ["echo PWNED > /tmp/gmyj/pwned.txt"]
| Load function | Result |
|---|---|
yaml.safe_load | rejected: ConstructorError: could not determine a constructor for the tag ...os.system |
yaml.load(..., Loader=FullLoader) | rejected |
yaml.unsafe_load | executed. The file pwned.txt containing PWNED was on the server afterwards. |
With unsafe_load (or plain yaml.load without a loader in PyYAML versions before 5.1), a YAML file from an untrusted source becomes a remote control for your server. For years this was one of the most common security holes in Python projects. Ruby’s YAML.load had the same problem.
JSON doesn’t have this problem because the format has no type tags. json.loads and JSON.parse can only produce the six basic types.
Practical rule: load untrusted YAML only with safe_load (Python) or the default load of js-yaml 4, which has been safe since version 4. And for anything arriving from outside through an API, just use JSON.
Comments, Multi-Line Strings and Readability: Where YAML Wins
After so many traps, fairness demands it: YAML has real advantages, and they’re the reason it’s everywhere in configuration.
Comments. This is the big one. In a config file you want to explain why a value is what it is. JSON doesn’t allow comments, a deliberate decision by Douglas Crockford. The result is workarounds like "_comment": "..." fields or variants like JSONC that many tools don’t understand.
Multi-line strings. A shell script in a CI file, a certificate, an SQL query: in YAML you just write it as a block.
script: |
npm ci
npm run build
rsync -a dist/ /var/www/
In JSON this would be one long string with \n in between, which nobody enjoys reading or editing.
Less noise. In indented JSON, a large share of our 50,000 records consisted of quotes, braces and commas. Machines don’t care; humans skimming a file notice the difference.
Reuse. Anchors and aliases, bomb notwithstanding, are genuinely useful for configs with many similar blocks. Docker Compose, GitLab CI and many other tools use them to cut repetition.
Multiple documents in one file. Kubernetes manifests often bundle a Deployment, Service and Ingress in a single file, separated by ---. In JSON you’d need a wrapping array or several files.
A thorough YAML introduction from TechWorld with Nana, with a focus on Kubernetes and Docker Compose. The video is older, but the syntax basics haven’t changed. The type traps from this article aren’t covered.
Where JSON Wins
Unambiguity. A JSON file means the same thing in every parser. Our type table above had three different results for the same YAML file. For JSON it would have a single column.
Speed. Parsing 10 to 220 times faster is not a detail when data flows in volume.
Security. No references, no type tags, no code execution. The worst a malicious JSON file can do is be large.
Tooling. Every programming language ships a JSON parser in its standard library. Browsers handle it natively. jq gives you a powerful command-line tool. JSON Schema is a mature standard for validation. For YAML you almost always need an third-party library, and which one you pick determines the behavior.
Errors surface. A missing or extra comma, a missing brace, a single quote: JSON stops and names the position. YAML often keeps guessing.
That’s exactly why nearly all web APIs are JSON-based. How interfaces work in general is explained in What is an API?, and why even GraphQL returns JSON in the end is covered in REST vs GraphQL.
Who Uses What? A Look at Real-World Practice
The division of labor is remarkably clear. We counted files in our own workspace (excluding node_modules and .git):
| Format | Number of files |
|---|---|
.json | 12,528 |
.yaml and .yml | 379 |
.toml | 5 |
JSON outnumbers YAML by more than 30 to 1. Most of those JSON files are data written by programs: package metadata, state files, exports, snapshots. The YAML files are almost exclusively hand-written configuration: Compose stacks, CI workflows, translation files.
The wider software world looks the same:
- YAML: Docker Compose, Kubernetes, Helm, GitHub Actions, GitLab CI, Ansible, Home Assistant, OpenAPI specs (often), frontmatter in Markdown files (this article starts with YAML too).
- JSON: REST APIs,
package.json,tsconfig.json,composer.json, browser storage, NDJSON log files, JSON/JSONB database columns, VS Code settings. - TOML: Rust (
Cargo.toml), Python projects (pyproject.toml), Hugo. TOML is the third way: comments like YAML, but far less type guessing.
The rule of thumb that follows: if a human writes the file and a program reads it, YAML (or TOML) is comfortable. If a program writes it and a program reads it, JSON is the better choice.
Converting YAML to JSON and Back
Since YAML 1.2 is a superset of JSON, converting JSON to YAML is always lossless. The other direction loses comments, anchors and multi-document structure, and the types are whatever the parser guessed.
On the command line:
# YAML → JSON with Python (PyYAML, beware: YAML 1.1 types!)
python3 -c 'import sys,yaml,json; print(json.dumps(yaml.safe_load(sys.stdin), indent=2, default=str))' < config.yaml
# YAML → JSON with Node (js-yaml, YAML 1.2)
npx js-yaml config.yaml > config.json
# JSON → YAML with yq (Mike Farah's Go version)
yq -p json -o yaml '.' config.json > config.yaml
A tip from our tests: convert a YAML file to JSON once before you trust it. In the JSON output you see immediately whether NO is a string or false, whether the postal code kept its zero, and whether a key ended up on the wrong level. For Docker Compose, docker compose config does exactly that: it shows you the file as Compose understood it.
And a warning about online converters: config files often contain passwords, tokens and internal hostnames. Those don’t belong in a form on someone else’s website. The commands above run locally.
How to Write YAML Without Nasty Surprises
Our measurements boil down to seven rules that avoid nearly every problem:
- Quote everything that is text but looks like something else. Country codes, postal codes, versions, times,
yes/no/on/off, phone numbers, digit-only hashes. - Spaces only, never tabs, and consistently two spaces per level. Configure your editor to show whitespace.
- Use a linter.
yamllintflags tabs, indentation errors, duplicate keys and the dangerous booleans. Itstruthyrule warns about exactly the Norway problem. - Know which parser reads your file. PyYAML (1.1) and js-yaml (1.2) behave differently on the same file. When in doubt, convert to JSON and look.
- Load untrusted YAML safely.
safe_loadin Python, neverunsafe_loador a full loader for data you didn’t write yourself. - Use anchors sparingly, and for untrusted input pick a library with an alias limit (like
yaml2.x for Node). - Validate against a schema. Many tools publish JSON Schemas for their YAML files (Compose, GitHub Actions, Kubernetes). Editors like VS Code with the YAML extension then flag errors as you type.
When to Use YAML, When to Use JSON: The Decision

Choose JSON when:
- Programs write and read the data (APIs, exports, caches, logs, messages between services).
- The data is large or read frequently.
- The data comes from third-party or untrusted sources.
- Several programming languages read the same file and must agree with certainty.
- You’re working in the browser.
Choose YAML when:
- Humans write and maintain the file (configuration, CI, infrastructure).
- You need comments to explain decisions.
- The tool prescribes it (Compose, Kubernetes, GitHub Actions). Then don’t argue; write clean YAML.
- Multi-line strings like scripts or certificates are part of the data.
Consider TOML when: you’re starting a new project, the config is flat to moderately nested, and you want comments without type guessing.
A direct comparison by The Git Guild from 2020. It’s an older video, but the differences between the formats haven’t changed since.
Checklist: YAML or JSON in Five Questions
| Question | Leans YAML | Leans JSON |
|---|---|---|
| Who writes the file? | a human | a program |
| Do I need comments? | yes | no |
| How large is the data? | small (configuration) | any size, including very large |
| Where does the data come from? | my own repository | also from outside |
| Does the tool dictate a format? | Compose, Kubernetes, CI | APIs, Node, PHP, TS projects |
If three or more answers land in one column, the decision is made.
Frequently Asked Questions About YAML vs JSON
What is the difference between YAML and JSON?
JSON is a strict data format with brackets and quotes, well suited to exchanging data between programs. YAML structures data through indentation, allows comments and multi-line strings, and is meant for configuration written by humans. YAML guesses data types from the value; JSON defines them unambiguously.
Is YAML better than JSON?
Not in general. YAML is nicer for humans to read and write; JSON is faster, unambiguous and safer. In our test JSON parsed the same data 10 to 220 times faster. For configuration YAML is often the better choice, for data and APIs JSON is.
Is JSON valid YAML?
Yes. Since YAML 1.2, every valid JSON document is also valid YAML, so you can load a JSON file with a YAML parser. Older YAML 1.1 parsers like PyYAML can deviate in rare edge cases.
What does YAML stand for?
Today YAML stands recursively for “YAML Ain’t Markup Language”. Originally it was “Yet Another Markup Language”. The rename emphasized that YAML is a data format, not a markup language like HTML or XML.
What is the difference between .yml and .yaml?
There is none. Both extensions denote the same format. The official recommendation is .yaml; .yml dates back to when file extensions were limited to three characters. Docker Compose now prefers compose.yaml.
Why does NO become false in YAML?
Because YAML 1.1 interprets a long list of words as booleans: yes, no, on, off, y, n in various spellings. YAML 1.2 only knows true and false, but parsers like PyYAML still follow 1.1. In our test the country code NO became false there. The fix is quotes: "NO".
Can you write comments in JSON?
No, the JSON standard doesn’t allow comments. Some tools accept the JSONC variant with // comments, for example VS Code and TypeScript for tsconfig.json. If you regularly need comments, YAML or TOML is usually the better fit.
Is YAML faster than JSON?
No, it’s considerably slower. In our test with 50,000 records JSON.parse took 30 ms, js-yaml 308 ms, the yaml library 4.2 seconds and pure-Python PyYAML 20 seconds. For small config files it doesn’t matter; for large data volumes it does.
Is YAML insecure?
YAML itself isn’t, but some parser functions are. With Python’s yaml.unsafe_load our test file executed a shell command on the server. On top of that, anchors and aliases can turn 254 bytes into over 22 MB of data. With safe_load and libraries that cap aliases, YAML is safe to use.
How do I convert YAML to JSON?
Locally with a one-liner: in Python with yaml.safe_load and json.dumps, in Node with npx js-yaml file.yaml, or with the yq tool. Don’t paste config files containing passwords into online converters.
Why does Docker Compose use YAML instead of JSON?
Because Compose files are written and maintained by humans. Comments, multi-line commands and anchors for reuse make them more readable than JSON. Since JSON is valid YAML, Compose will also read a JSON file passed with -f, but practically nobody does that.
Conclusion
The “YAML vs JSON” question is really the question of who writes the file. Our measurements make the price of YAML’s convenience very clear: 10 to 220 times slower parsing, three parsers giving three different answers for the same file, a country code that turns into false, a time that turns into 1342, and a 254-byte file that swells to 23 MB. In return you get comments, multi-line strings and files a human understands at a glance.
Our recommendation: JSON for data, YAML for configuration. And when you write YAML, quote everything that could look like something else, use a linter, and load with safe_load. Do that and YAML is a pleasant format. Don’t, and one day you’ll be hunting for the bug that made Norway disappear from your country list.