YAML is everywhere — Kubernetes, Docker Compose, Ansible, GitHub Actions, CI/CD pipelines. It looks friendly until you spend an hour debugging why on became true or your port number turned into octal.
Here are the traps and how to avoid them.
The Norway Problem#
1
2
3
4
5
| # What you wrote
country: NO
# What YAML parsed
country: false
|
YAML interprets NO, no, No, OFF, off, Off as boolean false. Same with YES, yes, Yes, ON, on, On as true.
Fix: Quote strings that could be boolean:
1
2
| country: "NO"
enabled: "yes" # If you mean the string "yes"
|
Numbers That Aren’t#
Octal Surprise#
1
2
3
4
5
| # What you wrote
port: 0800
# What YAML parsed
port: 512 # Interpreted as octal!
|
Leading zeros make numbers octal in YAML 1.1.
Fix:
1
2
| port: 800 # Remove leading zero
port: "0800" # Or quote it
|
Version Numbers#
1
2
3
4
5
| # What you wrote
version: 1.0
# What YAML parsed
version: 1 # Float, loses the .0
|
1
2
3
4
5
| # What you wrote
version: 3.10
# What YAML parsed
version: 3.1 # Trailing zero dropped
|
Fix: Always quote version numbers:
1
2
| version: "1.0"
version: "3.10"
|
Scientific Notation#
1
2
3
4
5
| # What you wrote
password: 1e10
# What YAML parsed
password: 10000000000.0 # Scientific notation!
|
Fix:
Multiline Strings#
Literal Block (|)#
Preserves newlines exactly:
1
2
3
4
| script: |
echo "line 1"
echo "line 2"
echo "line 3"
|
Result: "echo \"line 1\"\necho \"line 2\"\necho \"line 3\"\n"
Folded Block (>)#
Folds newlines into spaces:
1
2
3
4
| description: >
This is a long
description that will
become one line.
|
Result: "This is a long description that will become one line.\n"
Chomping Indicators#
1
2
3
4
5
6
7
8
9
10
11
| # Keep final newline (default)
text: |
content
# Strip final newline
text: |-
content
# Keep all trailing newlines
text: |+
content
|
Indentation Hell#
YAML uses spaces, not tabs. And indentation matters.
1
2
3
4
5
6
7
8
9
10
11
12
| # Valid
parent:
child: value
# Invalid (tabs)
parent:
child: value # TAB character - will fail
# Invalid (inconsistent)
parent:
child1: value
child2: value # 3 spaces instead of 2
|
Configure your editor:
- Spaces only, no tabs
- Show whitespace characters
- Consistent indent (2 spaces is common)
Empty Values#
1
2
3
4
5
6
| # These are all different
key1: # null
key2: "" # empty string
key3: null # explicit null
key4: ~ # also null
key5: "null" # the string "null"
|
Anchors and Aliases#
Useful but confusing:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Define anchor
defaults: &defaults
adapter: postgres
host: localhost
# Use alias
development:
<<: *defaults
database: dev_db
production:
<<: *defaults
database: prod_db
host: prod-server
|
Expands to:
1
2
3
4
5
6
7
8
9
| development:
adapter: postgres
host: localhost
database: dev_db
production:
adapter: postgres
host: prod-server # Overridden
database: prod_db
|
Special Characters in Keys#
1
2
3
4
| # These need quotes
"key:with:colons": value
"key with spaces": value
"key#with#hashes": value
|
1
2
3
| # This is a comment
key: value # This is also a comment
key2: "value # but this is part of the string"
|
Boolean Explosion (YAML 1.1)#
All of these are true:
1
2
3
4
5
6
7
8
9
| - true
- True
- TRUE
- yes
- Yes
- YES
- on
- On
- ON
|
All of these are false:
1
2
3
4
5
6
7
8
9
| - false
- False
- FALSE
- no
- No
- NO
- off
- Off
- OFF
|
YAML 1.2 fixed this (only true/false), but many parsers still use 1.1.
Colon in Values#
1
2
3
4
5
| # Broken
message: Error: something went wrong
# Fixed
message: "Error: something went wrong"
|
Colons followed by space start a mapping.
Timestamps#
1
2
3
4
5
6
7
| # These become datetime objects
date1: 2024-01-15
date2: 2024-01-15 10:30:00
# These stay strings
date3: "2024-01-15"
date4: 15-01-2024 # Not ISO format
|
Safe Practices#
Always Quote#
When in doubt, quote:
1
2
3
4
5
| # Safe
version: "1.0"
country: "NO"
port: "0800"
password: "1e10"
|
Use a Linter#
1
2
3
4
5
6
7
| # yamllint
pip install yamllint
yamllint config.yml
# Or in CI
- name: Lint YAML
run: yamllint -c .yamllint.yml .
|
.yamllint.yml:
1
2
3
4
5
6
| extends: default
rules:
line-length:
max: 120
truthy:
check-keys: true
|
Validate Before Deploy#
1
2
3
4
5
6
7
8
| # Kubernetes
kubectl apply --dry-run=client -f config.yml
# Docker Compose
docker compose config
# Ansible
ansible-playbook --syntax-check playbook.yml
|
Use Schema Validation#
1
2
3
4
5
6
7
8
9
10
| # JSON Schema for YAML
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
properties:
version:
type: string
port:
type: integer
required:
- version
|
Debugging Tips#
Parse and Dump#
1
2
3
4
5
6
| import yaml
# See what YAML actually parsed
with open('config.yml') as f:
data = yaml.safe_load(f)
print(repr(data))
|
1
2
| # One-liner
python3 -c "import yaml; print(yaml.safe_load(open('config.yml')))"
|
Use yq#
1
2
3
4
5
| # Pretty print
yq . config.yml
# Check specific value type
yq '.port | type' config.yml
|
Quick Reference#
| Gotcha | Example | Fix |
|---|
| Boolean strings | NO → false | "NO" |
| Octal numbers | 0800 → 512 | 800 or "0800" |
| Version floats | 1.0 → 1 | "1.0" |
| Scientific | 1e10 → 10000000000 | "1e10" |
| Colons | msg: a: b → error | "a: b" |
| Timestamps | 2024-01-15 → datetime | "2024-01-15" |
YAML’s implicit typing is the root of most problems. When you’re not sure how something will be interpreted, quote it. A little extra typing beats hours of debugging why your Norwegian customers broke the build.