YAML vs JSON vs TOML: When to Use Which Configuration Format
Every project eventually has a config problem. You need to store database credentials, define deployment environments, describe a CI pipeline, or wire up dependencies β and someone has to pick a format. JSON? YAML? TOML? The choice feels arbitrary until it bites you.
Docker Compose files are YAML. package.json is JSON. Rust's dependency manifest (Cargo.toml) is TOML. Python packaging (pyproject.toml) is TOML. GitHub Actions workflows are YAML. These are not random β each format won the adoption battle in its domain for specific reasons. Understanding those reasons lets you make a deliberate call instead of defaulting to "whatever the template used."
This article is a decision guide. By the end, you will know the real tradeoffs of each format, see the exact YAML footguns that have caused production incidents, and have a clear framework for choosing.
The Common Ancestor: INI Files
Before diving into the modern contenders, it is worth spending thirty seconds on INI files β the format all three are, in some sense, reacting to.
INI files look like this:
[database]
host = localhost
port = 5432
name = mydb
[server]
port = 8080
debug = trueSimple, human-readable, supports sections, supports comments. The problems are equally simple: no standard spec (every parser interprets edge cases differently), no nested structures beyond one level, no arrays, no explicit types. Everything is a string unless your application decides otherwise.
INI was fine for php.ini in 1997. It is not fine for describing a Kubernetes deployment in 2026. But it identified the right goals β human editability and comments β that the modern formats either inherited or discarded.
JSON: The Lingua Franca of Data
JSON (JavaScript Object Notation) was standardized in the early 2000s and became the dominant wire format for APIs. Its strength comes from its constraints.
What JSON gets right
JSON has exactly six value types: string, number, boolean, null, array, and object. The spec is unambiguous. Every compliant JSON parser will parse the same document identically. There are no surprises.
{
"database": {
"host": "localhost",
"port": 5432,
"ssl": true
},
"allowed_origins": ["https://example.com", "https://api.example.com"],
"timeout_ms": null
}The type of port is unambiguously a number. ssl is unambiguously a boolean. allowed_origins is unambiguously an array of strings. A JSON parser cannot misread this.
JSON is also ubiquitous. Every language has a standard library JSON parser. Browsers parse it natively. Databases like PostgreSQL have native JSON columns. It is the lowest-common-denominator format for data exchange between systems that do not share a runtime.
What JSON gets wrong for configuration
JSON has no comment syntax. This is a deliberate design decision by Douglas Crockford (JSON's creator), on the grounds that comments in data formats encourage people to put parsing directives in them, which corrupts the portability of the format.
That is a fine principle for API responses. It is a serious problem for config files. Config files need comments. You need to explain why timeout_ms is 30000 and not 10000. You need to mark which fields are safe to change. You need to leave a note that says "do not set this above 100 on staging."
The workarounds are all bad. Some teams use "_comment" fields. Some use JSON5 (a superset with comments) and hope their tooling handles it. Some use JSONC (JSON with Comments, popularized by VS Code's settings.json). None of these are standard JSON.
JSON is also verbose for configuration. Trailing commas are not allowed, which causes constant syntax errors when reordering fields. Every string key requires double quotes. There is no multiline string syntax β a long shell command has to be escaped onto one line.
JSON belongs in: API responses, data interchange between services, package manifests that are primarily machine-written (package.json, composer.json), and anywhere you need guaranteed parse consistency across heterogeneous systems.
You can validate JSON online to catch syntax errors before they reach your application β which matters more than you might expect, since a single missing comma or trailing bracket will break a JSON parser entirely.
YAML: Power and Peril
YAML (YAML Ain't Markup Language) was designed to be the human-friendly alternative to XML. It is expressive, readable, and deeply used in DevOps tooling. It is also the most dangerous config format in common use.
What YAML gets right
YAML allows comments (#). It infers types from unquoted values, so you can write port: 5432 instead of "port": 5432. It supports multiline strings with clean heredoc-like syntax. It is concise: indentation replaces braces and brackets.
database:
host: localhost
port: 5432
ssl: true
allowed_origins:
- https://example.com
- https://api.example.com
# This is a comment explaining why the timeout is high
timeout_ms: 30000For CI/CD configurations β GitHub Actions, GitLab CI, CircleCI β YAML's ability to express nested structures cleanly, with comments, is genuinely useful. A GitHub Actions workflow file often spans hundreds of lines with nested conditions, matrix strategies, and environment variable blocks. JSON would be unreadable. TOML would be awkward for deeply nested structures.
The YAML footguns
YAML's type inference is where things go wrong. The parser looks at unquoted values and decides what type they are. The decisions it makes are not always what you expect.
The Norway Problem. In YAML 1.1 (which is what most parsers implement β not YAML 1.2), a bare NO is parsed as the boolean false. So is no, No, n, N, false, False, FALSE, off, Off, and OFF. A list of ISO 3166-1 country codes includes NO for Norway. A config file with a country code list will silently turn Norway into false.
# YAML 1.1 β what the parser actually sees
country: NO # β false (boolean)
enabled: yes # β true (boolean)
mode: on # β true (boolean)
debug: off # β false (boolean)The fix is to quote everything that should be a string: country: "NO". But the whole appeal of YAML is that you do not have to quote things. Requiring quotes to avoid surprises erodes the format's readability advantage.
Octal integers. In YAML 1.1, a number starting with 0 is treated as octal. File permissions are commonly written in octal: chmod: 0755. A YAML parser reads 0755 as the decimal number 493, not 755. This has broken real deployment scripts.
# YAML 1.1 β octal interpretation
file_mode: 0755 # β 493 (decimal equivalent of octal 0755)Multiline string ambiguity. YAML offers multiple multiline string syntaxes β literal block (|), folded block (>), and inline. They behave differently regarding newlines, trailing whitespace, and indentation. Getting the wrong one silently produces the wrong string.
# Literal block β preserves newlines
command: |
npm install
npm run build
# Folded block β newlines become spaces (useful for long prose)
description: >
This is a long description that will be
folded into a single line with spaces.Tabs vs spaces. YAML does not allow tab characters for indentation. An editor that inserts tabs silently will produce a parse error β or worse, a different parse depending on the tab stop setting.
YAML 1.2 fixed many of these issues (including the boolean value list), but most tools still implement 1.1. When you use YAML, you are writing for the parser your toolchain happens to ship, which may or may not match your assumptions.
TOML: Designed for Configuration
TOML (Tom's Obvious Minimal Language) was created in 2013 by Tom Preston-Werner (co-founder of GitHub) specifically to be a config file format that humans can write and machines can parse without ambiguity.
What TOML gets right
TOML has explicit, unambiguous types. A string is always quoted. A number is always a number. A boolean is always true or false (lowercase, no aliases). There is no type inference from bare values, so there is no Norway Problem.
[database]
host = "localhost"
port = 5432
ssl = true
allowed_origins = [
"https://example.com",
"https://api.example.com",
]
# This is a comment explaining why the timeout is high
timeout_ms = 30000TOML has first-class support for dates and times (publishedAt = 2026-04-04), which JSON and YAML leave as strings that applications parse themselves.
Section headers ([database], [[servers]]) are explicit and readable. Dotted keys (database.host = "localhost") let you express nested values inline. Array of tables ([[servers]]) gives you a clean list-of-objects syntax.
[[servers]]
name = "production"
host = "prod.example.com"
port = 443
[[servers]]
name = "staging"
host = "staging.example.com"
port = 443The TOML spec is precise and versioned. Every conformant TOML parser produces the same result for the same input.
Where TOML falls short
TOML works well for shallow-to-moderate nesting. For deeply nested structures β the kind that appear in Kubernetes manifests or Ansible playbooks β the section header syntax becomes repetitive. YAML's indentation-based nesting scales more naturally to six or seven levels deep.
TOML also has lower tooling support than JSON or YAML. Most languages have excellent TOML libraries, but editor support (syntax highlighting, validation, autocomplete) is less consistent than JSON's.
Comparison Table
The Decision Framework
Once you strip away the marketing language, the choice comes down to three questions.
Is this primarily machine-generated or human-written?
If the file is generated by code and consumed by code β an API response, a build artifact, a serialized cache entry β use JSON. It is the most portable, the most parseable, and tooling support is universal. You do not need comments in a file no human reads. Compressing these JSON files before transferring them over a network can meaningfully reduce payload size.
If a human writes and edits this file regularly β a project config, an environment description, a tool manifest β comments are non-negotiable. That rules out plain JSON.
Do you control the toolchain?
If you are locked into an ecosystem that mandates a format β Kubernetes requires YAML, GitHub Actions requires YAML, Cargo requires TOML, package.json is JSON β there is no decision to make. Use what the ecosystem expects.
If you are designing a new tool or a new config file from scratch, you have a real choice.
How deep is the nesting, and how tolerant is your team of subtle bugs?
YAML is expressive but fragile. A team that writes careful YAML, quotes all strings, runs a linter, and understands the 1.1 versus 1.2 distinction can use it safely. A team that is moving fast, has diverse editor setups, and does not run YAML linting in CI will eventually hit a Norway Problem or an octal bug in production.
TOML is the safer choice for human-written config when you control the format. Explicit types mean the parser error surfaces at parse time, not at runtime when Norway has become false and your feature flag logic silently misfires.
Quick reference
- API response or data interchange between services: JSON
- Package manifest (primarily machine-written): JSON
- Kubernetes, Docker Compose, GitHub Actions, Ansible: YAML (not a choice)
- New project config file you are designing: TOML
- Rust project: TOML (
Cargo.toml) - Python project: TOML (
pyproject.toml) - Legacy application config with simple key-value needs: INI (or migrate to TOML)
Validating Your Config Files
Syntax errors in config files are some of the most frustrating bugs to diagnose β the application fails to start, the error message is cryptic, and the mistake is a single misplaced character.
For JSON, you can validate and pretty-print JSON online to catch errors immediately. Pasting a JSON config file into a validator before committing it takes five seconds and prevents a class of CI failures. For deeper JSON workflows β minifying for production, diffing config changes between environments β see our guide on how to validate, minify, and pretty-print JSON.
For YAML, yamllint is the standard CLI linter. Running it in CI is not optional if your team writes production YAML. For TOML, most language ecosystems ship a built-in validator (cargo check validates Cargo.toml implicitly; python -c "import tomllib; tomllib.load(open('pyproject.toml', 'rb'))" validates any TOML file directly).
The Format Is a Contract
Whichever format you pick, it becomes a contract with every developer and tool that touches your project. Changing it later is painful β not impossible, but painful. The migration from setup.cfg and setup.py to pyproject.toml in the Python ecosystem took years and still is not complete.
Pick deliberately. JSON when machines are the primary consumer and portability is critical. YAML when the ecosystem mandates it or when deeply nested structures need to be readable. TOML when humans write the file, correctness matters, and you want the parser to catch ambiguity at read time rather than surfacing it as a runtime bug six months later.
The config format is not just a file format. It is a statement about who edits the file, what tools process it, and how much you trust implicit type coercion. Make that statement on purpose.
Need to validate a JSON config file right now? Try the FileCrank JSON Viewer β paste your JSON, catch errors instantly, and pretty-print it for readability.