Pick up any Rust project and you'll find a Cargo.toml. Clone a GitHub repo and
you'll find a .github/workflows/ folder full of YAML. Both formats are doing the same
surface-level job — storing structured configuration that humans edit — but they make very different
tradeoffs. If you've ever had YAML silently mangle a value on you, or wondered why Rust picked TOML
instead of YAML for its package manifest, this is the article for you.
The Core Difference: Explicit vs Implicit
The fundamental split between TOML and YAML is about how much the parser is allowed to
guess. TOML is explicit: every
value has an unambiguous type. Strings are always quoted. Booleans are exactly true
or false. Datetimes are first-class values with their own syntax. There is no
implicit type coercion — the parser does not try to be clever.
YAML leans the other way.
It tries to be convenient: you don't need quotes around most strings, and the parser infers types
from the value's appearance. That inference is what bites people. The YAML 1.1 spec
(still used by many tools) treats yes, no, on, off,
true, and false all as booleans. It treats unquoted version strings like
1.0 as floats. And then there's the Norway Problem.
YAML's Famous Footguns
The Norway Problem became something of a meme in DevOps circles. In YAML 1.1, the country
code NO parses as boolean false. So a config mapping ISO country codes
to settings would silently convert Norway's entry into a boolean. The
YAML 1.2 spec fixed this,
but many widely-used parsers — including PyYAML's default mode until recently — still target YAML 1.1.
Check which spec version your tooling actually implements before you trust it.
NO,
Yes, on, and off all become booleans. The fix is simple —
quote your strings — but the problem is that it's silent. Your config loads without error and you
get a boolean where you expected a string.# YAML 1.1 implicit type coercion — all of these silently become booleans:
countries:
norway: NO # → false ← The Norway Problem
sweden: SE # → "SE" (fine, not in the boolean list)
enabled: yes # → true
disabled: no # → false
feature_flag: on # → true
another: off # → false
# Version strings can become numbers:
python_version: 3.10 # → float 3.1 (trailing zero dropped)
api_version: 1.0 # → float 1.0
# Safe: quote anything that could be ambiguous
python_version: "3.10"
country: "NO"
enabled: "yes"- Octal literals. In YAML 1.1,
010parses as8(octal), not10. This matters for file permission values like0755. - Tab vs space. YAML forbids tab characters for indentation. Paste in code from an editor configured to use tabs and you get a cryptic parse error — or worse, silent misalignment.
- Implicit nulls. A key with no value becomes
null. Easy to create by accident when editing by hand. - Indentation sensitivity. One extra space and a value silently shifts from one parent to another. No error, just wrong data.
TOML: What Explicit Types Actually Look Like
TOML's type system is spelled out in the
TOML v1.0 spec. Strings must
be quoted (single or double). Booleans are true or false and nothing else.
Integers are integers. Floats are floats. And datetimes — something neither JSON nor YAML has as a
native type — are first-class values in TOML.
# TOML types are always unambiguous
name = "my-app" # string — must be quoted
version = "1.0.0" # string — quotes make it clear this is not a float
port = 8080 # integer
debug = false # boolean — only true/false, nothing else
threshold = 0.95 # float
# Datetime is a first-class type — no string parsing needed
created_at = 2024-01-15T09:30:00Z
build_date = 2024-03-20
# Arrays
allowed_hosts = ["localhost", "staging.example.com", "api.example.com"]
# Inline tables
[database]
host = "postgres.internal"
port = 5432
name = "payments_prod"
pool_size = 20The country code NO is just a string in TOML because it's not quoted, but TOML
doesn't infer types from string-like values — unquoted values follow strict syntactic rules. To be
a string, it needs quotes. TOML simply doesn't have the implicit coercion machinery that causes
YAML's footguns.
Side by Side: A CI Pipeline Config
Here's a GitHub Actions workflow — this is YAML's home territory. The format fits naturally because GitHub Actions expects YAML, the indentation-based nesting mirrors the logical structure, and comments are essential for explaining non-obvious step configurations.
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
build-and-publish:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build --if-present
- name: Publish to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}Now here's the equivalent project manifest in TOML — this is where TOML shines. Compare a real Cargo.toml structure with what the same config would look like in YAML:
# Cargo.toml — Rust package manifest
[package]
name = "payments-service"
version = "2.4.1"
edition = "2021"
description = "Payment processing microservice"
license = "MIT"
authors = ["Alice Chen <[email protected]>"]
[dependencies]
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls"] }
tracing = "0.1"
anyhow = "1.0"
[dev-dependencies]
tokio-test = "0.4"
wiremock = "0.6"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1TOML's Weaknesses: Array of Tables
TOML isn't without rough edges. The syntax for arrays of tables — [[double brackets]]
— is one of the more confusing things in the spec. It's how you express what JSON would write as an
array of objects, and it reads strangely at first.
# TOML array of tables — [[double brackets]] creates array entries
[[server]]
host = "web-01.example.com"
port = 443
region = "us-east-1"
[[server]]
host = "web-02.example.com"
port = 443
region = "us-west-2"
[[server]]
host = "web-03.example.com"
port = 443
region = "eu-west-1"
# The above is equivalent to this JSON:
# { "server": [
# { "host": "web-01.example.com", "port": 443, "region": "us-east-1" },
# { "host": "web-02.example.com", "port": 443, "region": "us-west-2" },
# { "host": "web-03.example.com", "port": 443, "region": "eu-west-1" }
# ]}In YAML the same structure reads more naturally — just a list with indented properties.
For deeply nested data with repeated array entries, YAML's indentation-based nesting is genuinely
less verbose than TOML's [[section]] headers. TOML also has no equivalent to YAML's
anchor-and-alias system, so you can't define a shared block once and reference it elsewhere. If
you find yourself duplicating the same set of values in multiple TOML tables, you're stuck copying
them manually.
Where Each Format Wins in Practice
YAML wins by default in these ecosystems:
- GitHub Actions. The entire workflow syntax is YAML. You don't get a choice here, and it's fine — the indentation maps well to the nested structure of jobs, steps, and conditions.
- Kubernetes. Every manifest — Deployments, Services, ConfigMaps, Ingress rules — is YAML. The Kubernetes object model is deeply nested, and YAML handles that gracefully.
- Docker Compose. Service definitions, networks, volumes — all YAML. Comments explaining why a port is exposed or why a specific healthcheck interval is used are part of the documentation.
- Ansible. Playbooks, roles, variable files — YAML throughout. The comment support is actively used to explain non-obvious task parameters.
TOML wins when you control the format:
- Rust projects.
Cargo.tomlis the gold standard. Dependency declarations, feature flags, build profiles — all in TOML. The explicit types mean version strings like"1.0.0"stay strings. - Python projects.
pyproject.tomlhas become the standard for Python project metadata, build config, and tool settings (Black, isort, mypy, pytest all read from it). - Tool configuration where ambiguity causes bugs. If your config contains version strings, country codes, or other values that look like they might be booleans or numbers, TOML's explicit types eliminate a whole category of parsing surprises.
- Flat configs. When your config is mostly key-value pairs at one or two levels of nesting, TOML is more readable than YAML and cleaner than JSON.
The Practical Decision Guide
Most of the time the choice is made for you by the ecosystem. GitHub Actions is YAML. Kubernetes is YAML. Rust is TOML. Python tooling is TOML. When you actually have a free choice, use this as your guide:
- Use YAML when the tooling mandates it — CI/CD platforms, Kubernetes, Helm charts, Docker Compose, Ansible. Fighting the convention costs more than it saves.
- Use YAML when you need anchors and aliases to keep a complex config DRY — there's no TOML equivalent.
- Use TOML for project manifests and tooling config you control — package metadata, linter settings, build configuration.
- Use TOML when your config contains values that YAML 1.1 might misinterpret — version strings, country codes, anything that resembles a boolean or number.
- Quote your YAML strings whenever the value could be confused with a boolean,
number, or null. Especially: version numbers, country codes, anything that starts with a digit,
and values like
yes,no,on,off.
Working with Both Formats
Need to validate or pretty-print a config file you're working on? The TOML Formatter handles TOML files, and the YAML Formatter covers YAML. If you need to migrate a config from one format to the other — say, converting a YAML-based tool config to TOML for a project that prefers it — the TOML to JSON and YAML to JSON converters both output JSON, which you can then convert to your target format. Sometimes going through JSON as an intermediate step is the most reliable path.
One thing worth knowing: both formats have their own JSON-compatible subsets. Valid JSON is valid YAML (YAML is a superset of JSON). TOML doesn't have that relationship with JSON, but the TOML spec is intentionally kept simple — it's a short read, which is more than you can say for the full YAML 1.2 specification.
Wrapping Up
TOML and YAML aren't really competing. They've settled into different niches based on their design priorities. YAML's implicit types and indentation-based structure make it the natural fit for large, nested configs maintained by teams — think Kubernetes manifests and GitHub Actions workflows. TOML's explicit types and flat section structure make it the natural fit for project manifests and tool configuration where a misread version string or country code would cause a real bug.
The one thing to carry with you: if you're writing YAML and you have any values that
look like they could be booleans, numbers, or nulls — quote them. It's the single habit that
prevents most YAML-related config bugs. And if you're starting a new project and get to choose
your config format freely, TOML is worth a look. The
TOML GitHub repository
has good examples, and once you've written a Cargo.toml or pyproject.toml,
the format's appeal becomes pretty clear.