YAML vs JSON vs TOML — which config format should you pick?
Compare YAML, JSON, and TOML for app configuration, CI/CD, and infrastructure-as-code by readability, expressiveness, parse strictness, and tooling. Includes YAML pitfalls like the Norway problem.
Four axes that drive the decision
Picking a configuration format is rarely just “which one reads best.” Four axes carry most of the weight. Readability depends on who edits the file and how often — application developers, SREs, or non-engineers all read these formats differently. Expressiveness covers nesting, arrays, comments, and multi-line strings — whether the format can describe the structure you actually have. Parser strictness covers implicit type coercion, trailing commas, whitespace sensitivity, and other foot-guns that turn into production incidents. Tooling and ecosystem support is often the deciding factor in practice: Kubernetes, Cargo, Node, and Python each have a first-class format they expect.
“YAML is readable”, “JSON is strict”, “TOML is modern” — none of these one-liners is wrong, and none is enough to choose. A Kubernetes manifest hinges on ecosystem and expressiveness; an API payload on strictness and interop; a Cargo manifest on readability and ecosystem.
Side-by-side comparison
| Property | YAML | JSON | TOML |
|---|---|---|---|
| Spec | YAML 1.2 (2009) | RFC 8259 (2017) | TOML 1.0.0 (2021) |
| Year introduced | 2001 | 2001 | 2013 |
| Comments | Yes (#) | No | Yes (#) |
| Trailing commas | N/A | No | Arrays only |
| Nesting | Significant indentation | Curly braces | Table headers [a.b.c] |
| Arrays | - or [...] | [...] | [...] |
| Multi-line strings | ` | >` blocks | Not directly (\n only) |
| Type inference | Implicit (foot-guns) | Explicit | Explicit (includes datetime) |
| Major adopters | Kubernetes / GitHub Actions / Docker Compose | Web APIs / package.json / VS Code | Cargo.toml / pyproject.toml / Hugo |
| Extensions | .yaml .yml | .json | .toml |
YAML’s most famous foot-gun is the Norway problem: YAML 1.1 coerces NO, yes, on, off and friends into booleans, so the unquoted country code NO becomes false. YAML 1.2 tightened this, but PyYAML and many other parsers still default to 1.1-compatible behaviour, so real bugs ship in real systems. Indentation drift — tabs mixed with spaces, sibling items at slightly different depths — is the other recurring source of incidents.
JSON’s spec is tiny and predictable, but as a config format it is genuinely awkward: no comments, trailing commas are syntax errors, multi-line strings must be written as \n escapes. That’s why tsconfig.json and friends quietly accept JSONC (JSON with comments), and JSON5 carved out a niche of its own.
TOML reads like an INI file and uses table headers such as [server.database] so that deep nesting does not depend on indentation. It became the de-facto choice the moment Cargo and Python’s pyproject.toml standardised on it. The trade-off is that arrays-of-tables ([[products]]) and dynamic, deeply nested trees get awkward fast.
Use case → recommended format
Kubernetes manifests, GitHub Actions, Docker Compose, Ansible playbooks: YAML, no real alternative — the tools only speak YAML. The defensive move is to always quote anything you mean as a string. Unquoted 1.10 becomes the float 1.1. Unquoted NO becomes false. Treat quoting as the rule, not the exception.
Web API payloads, anything that crosses a network, package.json: JSON. The smallest, most predictable spec; first-class parsers in every language; the lowest production blast radius. If you need to hand-edit, switch to JSONC or JSON5 during authoring but normalise back to plain JSON before commit so downstream tools never trip.
Cargo, pyproject, Hugo configs, personal tool configs: TOML. Optimised for one or two levels of nesting with natural comments. pyproject.toml is a good example — build settings, dependencies, and tool configs co-exist cleanly thanks to table headers.
VS Code, Neovim, ESLint configs: JSON or JSONC / JSON5. Editors tolerate the extensions, so use the comments. For shared projects, down-convert to plain JSON in CI or commit as .json5 so reviewers know what to expect.
Browser-only conversion and the foot-guns to remember
Hand-converting between the three formats is where type coercion bugs creep in. yaml-json-convert round-trips YAML and JSON with formatted output. For TOML, use toml-json-convert and treat JSON as the hub — going YAML to JSON to TOML in two hops is more reliable than searching for a direct converter. Need to tidy the final JSON? Run it through json-format to normalise indentation and key order.
The reason to convert in the browser is that configuration files routinely contain sensitive material — production hostnames, internal API endpoints, service-account names. Upload-style converters can log inputs server-side, and many terms of service explicitly carve out an analysis-and-improvement right over what you paste. The source for these tools is auditable on GitHub, and the DevTools Network tab makes it trivial to confirm nothing leaves the page during a conversion.
One more YAML rule worth keeping: never call yaml.load on untrusted input. PyYAML’s load allows arbitrary Python object construction by default and has been responsible for many remote-code-execution disclosures. Use yaml.safe_load, gopkg.in/yaml.v3 in strict mode for Go, or js-yaml’s safe APIs in Node — by default, not as an opt-in.