All three describe the same thing, nested keys and values. They differ in who they're written for.
- JSON: written for machines. Strict, unambiguous, no comments.
- YAML: written for humans, with indentation instead of brackets. Flexible enough to surprise you.
- TOML: written for humans editing config files. Explicit and boring, which is the point.
The same config, three ways
JSON
{ "name": "api", "port": 8080, "debug": false, "database": { "url": "postgres://localhost/app", "pool": 10 }, "allowedOrigins": ["https://example.com", "https://admin.example.com"] }
YAML
# service config name: api port: 8080 debug: false database: url: postgres://localhost/app pool: 10 allowedOrigins: - https://example.com - https://admin.example.com
TOML
# service config name = "api" port = 8080 debug = false allowedOrigins = ["https://example.com", "https://admin.example.com"] [database] url = "postgres://localhost/app" pool = 10
Side by side
| JSON | YAML | TOML | |
|---|---|---|---|
| Comments | No (JSONC/JSON5 yes) | Yes | Yes |
| Strings need quotes | Always | Usually not | Always |
| Whitespace matters | No | Yes, indentation is structure | No |
| Deep nesting | Fine | Fine | Gets awkward |
| Type surprises | None | Several (see below) | Very few |
| Native in browsers/JS | Yes | No | No |
| Common homes | APIs, package.json, tsconfig.json | Kubernetes, GitHub Actions, Docker Compose | Cargo.toml, pyproject.toml, Hugo |
The YAML traps
YAML's flexibility is where the bugs come from.
Unquoted values get guessed. Older YAML 1.1 parsers, which are still common, read these as booleans:
country: NO # false, not "NO" (the "Norway problem") enabled: yes # true answer: off # false
Numbers that aren't numbers.
version: 1.10 # the float 1.1, not "1.10" zip: 01234 # may be read as octal or lose the zero time: 12:30 # some parsers read this as a base-60 number
Indentation is meaning. One space off and a key moves to a different parent, often without an error.
Tabs aren't allowed for indentation.
The fix for most of these: quote strings that could look like something else, and run a linter (yamllint) in CI.
When each one wins
Choose JSON when a program writes the file, the data travels over a network, or you need every language to parse it identically. If you need comments in a JSON config, check whether your tool accepts JSONC (tsconfig.json does).
Choose YAML when the ecosystem already uses it. Don't fight Kubernetes or GitHub Actions. Quote strings, lint it, and keep files short.
Choose TOML when you're designing a new config file that people will edit by hand. It's hard to get wrong, has comments, and every value has an obvious type.
Converting between them
yq is jq for YAML (and it reads JSON and TOML too):
yq -o=json config.yaml > config.json yq -p=json -o=yaml config.json > config.yaml
To check JSON is valid before converting, paste it into the JSON Formatter. For querying JSON from the terminal, see the jq tutorial.