YAML Is Not "JSON but Prettier"

The common description — "YAML is human-readable, JSON is machine-readable" — obscures the real differences. YAML is an 80+ page specification with implicit type coercion, anchors, tags, merge keys, and multi-document support. JSON is a 1-page specification with no ambiguity.

This distinction matters because YAML's complexity creates type coercion traps, security vulnerabilities, and lossy conversion boundaries that don't exist in JSON.

The Norway Problem: Implicit Type Coercion

YAML's most infamous design flaw: bare values are implicitly coerced into typed values based on pattern matching.

Boolean Hell

In YAML 1.1 (used by most tools until recently), the following are all parsed as boolean true:

yaml
# All of these become boolean true in YAML 1.1
country_code: NO     # Norway's ISO code → false (!)
answer: yes          # → true
enabled: on          # → true
flag: TRUE           # → true
value: y             # → true

The "Norway problem": a country code list where NO (Norway) silently becomes false:

yaml
countries:
  - DK    # string "DK"
  - FI    # string "FI"  
  - NO    # boolean false (!)
  - SE    # string "SE"

After parsing: ["DK", "FI", false, "SE"]

Numeric Coercion

yaml
version: 1.0     # float 1.0, not string "1.0"
zipcode: 01onal  # string (not octal because of 'o')
port: 0755       # octal 493 in YAML 1.1 (!), string in 1.2
time: 12:30      # sexagesimal 750 in YAML 1.1 (!), string in 1.2

YAML 1.1 vs 1.2: The Critical Difference

Value YAML 1.1 interpretation YAML 1.2 interpretation
yes / no Boolean String
on / off Boolean String
y / n Boolean String
0755 Octal integer (493) String
1:30 Sexagesimal (90) String
true / false Boolean Boolean
null / ~ Null Null

YAML 1.2 fixed the worst coercion traps by limiting booleans to only true/false and integers to only decimal notation. But most YAML libraries still default to YAML 1.1 behavior (including PyYAML, Ruby's Psych, and older versions of js-yaml).

The Fix: Always Quote Ambiguous Values

yaml
# Safe: explicitly quoted strings
country_code: "NO"
version: "1.0"
port: "0755"
enabled: "yes"

Security: YAML Deserialization Attacks

YAML's tag system allows specifying arbitrary types, which in many languages means arbitrary code execution.

Python PyYAML: Code Execution via Tags

yaml
# DANGEROUS: executes os.system("rm -rf /") when loaded with yaml.load()
!!python/object/apply:os.system ["rm -rf /"]
python
import yaml

# VULNERABLE: yaml.load() with default Loader processes tags
data = yaml.load(malicious_yaml)  # arbitrary code execution!

# SAFE: yaml.safe_load() ignores custom tags
data = yaml.safe_load(yaml_string)  # only basic types

Always use safe_load() / SafeLoader. The yaml.load() without explicit Loader is the most common YAML vulnerability pattern.

YAML Bombs (Billion Laughs)

YAML anchors enable exponential expansion:

yaml
a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
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]

Five levels of 9× expansion: 9⁵ = 59,049 strings from a few lines. This can exhaust memory and crash the parser.

Defense: set recursion limits and maximum expansion size in your parser configuration.

JSON Is Immune

JSON has no tag system, no anchors, and no mechanism for code execution during parsing. This is a security advantage of JSON's simplicity.

What Is Lost in Each Conversion Direction

YAML → JSON (Information Loss)

YAML feature Behavior after conversion
Comments (#) Lost — no representation in JSON
Anchors & aliases Expanded into duplicate data (increases size)
Merge keys (<<) Expanded into flat objects
Multi-document (---) Lost — JSON has no multi-document concept
Custom tags (!!type) Lost or causes error
Multi-line scalars (|, >) Converted to single string with \n
Document markers (---, ...) Lost
Key ordering Implementation-dependent — JSON spec doesn't guarantee order

JSON → YAML (No Loss, but Choices)

JSON → YAML is lossless (YAML is a superset of JSON). But the conversion involves choices:

  • Indentation style (2 vs 4 spaces)
  • Flow vs block style for arrays and objects
  • Quote strategy (quote everything, or only when needed)
  • Whether to use anchors for repeated sub-trees

Parsing Library Security Postures

Library Language Default behavior Safe mode YAML version
PyYAML Python Unsafe (tags executed) safe_load() 1.1
ruamel.yaml Python Roundtrip-safe Configurable 1.2
strictyaml Python Safe (no tags, no implicit types) Always safe Subset
js-yaml JavaScript Safe (tags ignored by default) Default 1.2 (since v4)
go-yaml Go Safe (no arbitrary execution) Default 1.2 (v3)
SnakeYAML Java Unsafe (tags processed) SafeConstructor 1.1
Jackson YAML Java Configurable Via ObjectMapper 1.1

Recommendation

  • Python: Use strictyaml for config files (eliminates all implicit coercion), ruamel.yaml for roundtrip editing, never yaml.load() without SafeLoader
  • JavaScript: js-yaml v4+ is safe by default
  • Go: gopkg.in/yaml.v3 follows YAML 1.2, safe by default
  • Java: Never use new Yaml().load() on untrusted input — use SafeConstructor

When TOML Is the Better Choice

TOML (Tom's Obvious Minimal Language) was designed specifically for configuration files, avoiding both YAML's complexity and JSON's lack of comments:

toml
# TOML: explicit types, no coercion, comments supported
[server]
host = "localhost"
port = 5432
enabled = true

[database]
name = "myapp"
pool_size = 20

[[routes]]
path = "/api/users"
method = "GET"

[[routes]]
path = "/api/orders"
method = "POST"

YAML vs JSON vs TOML Decision Matrix

Criterion YAML JSON TOML
Human readability Good (if simple) Moderate Good
Machine parsing speed Slow Fast Moderate
Comments Yes No Yes
Type safety Poor (implicit coercion) Good (explicit) Good (explicit)
Security risk High (tags, bombs) Minimal Minimal
Spec complexity 80+ pages 1 page ~20 pages
Nested structures Unlimited depth Unlimited depth Awkward past 3 levels
Industry adoption Kubernetes, CI/CD, Ansible APIs, package.json Rust (Cargo.toml), Python (pyproject.toml)

Use YAML When

  • The ecosystem requires it (Kubernetes, GitHub Actions, Ansible)
  • You need multi-document files
  • You need anchors/aliases for DRY configuration

Use JSON When

  • Exchanging data between services (APIs)
  • The consumer is JavaScript/browser-based
  • You need schema validation (JSON Schema is mature)
  • Security is paramount (no code execution risk)

Use TOML When

  • Writing application configuration
  • You want comments without YAML's type coercion risks
  • Nesting is shallow (≤3 levels)
  • The ecosystem supports it (Rust, Python packaging, Hugo)

Correct Conversion Code

Python (Safe)

python
import json
from ruamel.yaml import YAML

yaml = YAML()
yaml.preserve_quotes = True

# YAML → JSON (safe, YAML 1.2)
with open('config.yaml') as f:
    data = yaml.load(f)
json_str = json.dumps(data, indent=2, ensure_ascii=False)

# JSON → YAML
with open('data.json') as f:
    data = json.load(f)
with open('output.yaml', 'w') as f:
    yaml.dump(data, f)

JavaScript / Node.js

javascript
import { load, dump } from 'js-yaml';
import { readFileSync, writeFileSync } from 'fs';

// YAML → JSON
const yamlContent = readFileSync('config.yaml', 'utf8');
const data = load(yamlContent);  // js-yaml v4: safe by default
const jsonStr = JSON.stringify(data, null, 2);

// JSON → YAML
const jsonContent = readFileSync('data.json', 'utf8');
const parsed = JSON.parse(jsonContent);
const yamlStr = dump(parsed, { indent: 2, lineWidth: 120 });

Go

go
package main

import (
    "encoding/json"
    "os"
    "gopkg.in/yaml.v3"
)

func yamlToJSON(yamlBytes []byte) ([]byte, error) {
    var data interface{}
    if err := yaml.Unmarshal(yamlBytes, &data); err != nil {
        return nil, err
    }
    // yaml.v3 uses map[string]interface{}, compatible with json.Marshal
    return json.MarshalIndent(data, "", "  ")
}

CLI Tools

bash
# yq: YAML swiss-army knife
yq -o=json config.yaml > config.json
yq -P config.json > config.yaml

# Python one-liner
python -c "import sys,yaml,json; print(json.dumps(yaml.safe_load(sys.stdin),indent=2))" < config.yaml

Common Pitfalls in Production

1. Indentation Errors Are Silent

YAML indentation determines structure. A single-space error changes meaning:

yaml
# Intended: nested under server
server:
  host: localhost
  port: 5432

# Bug: port is a sibling of server (wrong indentation)
server:
  host: localhost
port: 5432

2. Tabs vs Spaces

YAML forbids tabs for indentation. A tab character causes a parse error, but many editors display tabs and spaces identically.

3. Unquoted Strings That Look Like Other Types

yaml
version: 3.10    # float 3.1 (trailing zero dropped!)
version: "3.10"  # string "3.10" (correct)

4. Multiline String Trailing Newlines

yaml
# | preserves a trailing newline
content: |
  hello

# |- strips the trailing newline  
content: |-
  hello

The difference between | and |- matters for templates, scripts, and any value where a trailing newline changes behavior.

Summary

YAML and JSON are not interchangeable formats with different syntax. They have fundamentally different security models, type systems, and information capacity. Converting between them is lossy in the YAML→JSON direction (comments, anchors, tags, multi-document) and requires choices in the JSON→YAML direction (style, quoting, indentation).

Key principles:

  • Always use safe parsing functions (safe_load, SafeConstructor) for untrusted YAML
  • Quote values that could be implicitly coerced ("NO", "3.10", "yes")
  • Prefer YAML 1.2 libraries that eliminate boolean coercion traps
  • For new configuration files where ecosystem doesn't mandate YAML, consider TOML
  • Treat YAML→JSON conversion as lossy and validate the output