Every project needs a configuration format, and the choice usually comes down to JSON, YAML, or TOML. They can all represent the same data, but they have very different ergonomics — and the wrong choice creates friction that lasts the lifetime of the project.
The same config in three formats
Let's start with a concrete example. Here's a simple web server configuration expressed in all three:
JSON:
{
"server": {
"host": "0.0.0.0",
"port": 8080,
"workers": 4
},
"database": {
"url": "postgres://localhost:5432/mydb",
"pool_size": 10,
"ssl": true
},
"logging": {
"level": "info",
"format": "json"
}
}YAML:
server:
host: "0.0.0.0"
port: 8080
workers: 4
database:
url: postgres://localhost:5432/mydb
pool_size: 10
ssl: true
logging:
level: info
format: jsonTOML:
[server]
host = "0.0.0.0"
port = 8080
workers = 4
[database]
url = "postgres://localhost:5432/mydb"
pool_size = 10
ssl = true
[logging]
level = "info"
format = "json"At this scale, the differences are mostly cosmetic. The real divergence shows up when configs get complex.
Validate and format JSON configuration files instantly. Catches trailing commas, missing quotes, and syntax errors.
JSON: the universal interchange format
Strengths:
JSON is everywhere. Every programming language has a built-in JSON parser. Every API speaks JSON. Every developer can read it without learning a new syntax. There's no ambiguity — the spec (RFC 8259) is 10 pages long and covers every edge case.
JSON is also the safest format for machine-generated config. When your CI pipeline generates a config file, JSON's strict syntax means either the output is valid or it's not. There's no indentation to get wrong, no implicit type coercion, no gotchas.
Weaknesses:
No comments. This is JSON's biggest limitation for configuration files. You can't explain why a value is set to what it is, which makes configs harder to maintain over time. Various workarounds exist (JSONC, JSON5, "_comment" keys) but none are standard.
No trailing commas. Add a new key to the end of an object and you need to also add a comma to the previous line. This creates noisy diffs in version control — two lines changed when only one value was added.
Verbose for deeply nested structures. Every key must be quoted. Every string must be double-quoted (not single). Braces and brackets accumulate quickly.
Where it's used: package.json, tsconfig.json, composer.json, .eslintrc.json, AWS CloudFormation, Jupyter notebooks, VS Code settings.
YAML: the human-friendly format
Strengths:
YAML was designed for human readability, and it delivers. The indentation-based structure eliminates braces and brackets entirely. Comments are supported with #. Multi-line strings have clean syntax. The same data takes fewer characters and fewer lines.
YAML also supports features that JSON doesn't: anchors and aliases (for reusing values), merge keys (for inheriting config blocks), and multiple documents in one file (separated by ---).
Weaknesses:
YAML's flexibility is also its biggest risk. The spec is enormous — over 80 pages — and full of surprising behaviour.
The most infamous example is the Norway problem. In YAML, the bare value NO is interpreted as boolean false, not the string "NO". This means a list of country codes like [GB, US, NO, FR] silently becomes [GB, US, false, FR]. The same applies to yes, no, on, off, true, and false — all case-insensitive.
Other gotchas:
3.10becomes the float3.1, not the string "3.10" (breaks Python version numbers)08and09are invalid octal numbers in some parsers (breaks dates)- Tabs are not allowed for indentation, only spaces
- A single indentation mistake can silently restructure your entire config
- Different YAML libraries parse edge cases differently (YAML 1.1 vs 1.2)
Where it's used: Docker Compose, Kubernetes manifests, GitHub Actions workflows, Ansible playbooks, Helm charts, .gitlab-ci.yml, Swagger/OpenAPI.
TOML: the configuration-first format
Strengths:
TOML (Tom's Obvious Minimal Language) was designed specifically for configuration files, not for data interchange. It has clear, unambiguous types — strings are always quoted, booleans are always true/false, dates have first-class support (2026-04-03T10:00:00Z).
There's no indentation sensitivity. Sections are declared with [headers] like INI files, making the structure explicit. Comments use #. Multi-line strings and literal strings (no escape processing) are both supported.
TOML is the least surprising of the three formats. What you see is what you get — there's no implicit type coercion, no indentation gotchas, and the spec is short enough to read in one sitting.
Weaknesses:
Deeply nested structures become awkward. Each nesting level requires a new section header:
[server.tls.certificates.production]
cert_file = "/path/to/cert.pem"
key_file = "/path/to/key.pem"This is fine for 2-3 levels of nesting but gets unwieldy beyond that.
Arrays of tables have a non-obvious syntax:
[[server.routes]]
path = "/api/users"
handler = "users_handler"
[[server.routes]]
path = "/api/posts"
handler = "posts_handler"The double brackets [[]] mean "append to an array of tables." It's logical once you learn it, but it's not immediately obvious to newcomers.
TOML also has less ecosystem support than JSON or YAML. Not every language has a battle-tested TOML parser, though coverage has improved significantly since Rust and Python adopted it.
Where it's used: Cargo.toml (Rust), pyproject.toml (Python), Hugo, Deno, .goreleaser.yml, Netlify config.
The decision framework
The choice usually comes down to two questions:
Will humans edit this file regularly? If yes, avoid JSON (no comments, verbose). Choose YAML if you're in the Kubernetes/Docker/CI ecosystem where YAML is already the convention. Choose TOML if you're starting fresh and want fewer footguns.
Is this for data interchange or configuration? Data interchange (API responses, message queues, data files) → JSON, always. Configuration (app settings, deployment manifests, build config) → YAML or TOML.
| Criterion | JSON | YAML | TOML | | --- | --- | --- | --- | | Comments | No | Yes | Yes | | Human editing | Verbose | Good | Good | | Deep nesting | Good | Good | Awkward | | Type safety | Good | Poor (Norway problem) | Excellent | | Ecosystem | Universal | Strong (DevOps) | Growing (Rust, Python) | | Spec complexity | Simple (10 pages) | Complex (80+ pages) | Medium (30 pages) | | Machine generation | Excellent | Risky (indentation) | Good |
My recommendation
For new projects with simple config, use TOML. It's the most honest format — what you see is what the parser sees. The Rust and Python ecosystems have validated this choice at scale.
For Kubernetes, Docker, and CI/CD, use YAML because that's what the ecosystem expects. But always quote your strings, even when YAML doesn't require it. version: "3.10" is safer than version: 3.10. This single habit prevents most YAML-related bugs.
For data files, API contracts, and anything machines generate, use JSON. Its strictness is a feature — invalid JSON fails loudly and immediately.
And whatever you choose, validate your config files as part of your CI pipeline. A linter that catches a missing comma or a rogue boolean before deployment is worth more than the perfect format choice.
Validate YAML syntax, spot indentation errors, and convert between YAML and JSON.