TOML vs YAML vs JSON vs INI: choosing a config format
A practical comparison of TOML, YAML, JSON and INI for application config, with real examples of where each fits.
Published 2026-09-25
Four formats, one job
All four formats exist to let a human write down configuration that a program reads. They differ in how much structure they can express, how forgiving their syntax is, and how much they lean on convention versus explicit rules. There's no universally correct choice — the right one depends on how complex the config is and who's going to be hand-editing it.
JSON: strict, unambiguous, not meant for hand-editing
{
"service": "api",
"port": 8080,
"features": { "auth": true, "cache": false }
}
JSON has a small, precise grammar (see RFC 8259) — no comments, no trailing commas, every key quoted. That strictness is exactly why it's a poor fit for config files people edit by hand: a missing comma or trailing comma is an instant hard failure with no room for the parser to guess intent. It's a good fit when a program is the one writing the file — a lockfile, a generated manifest, an API-delivered config — because there's no ambiguity to resolve. If you need to review or diff a JSON config file, run it through a JSON validator first to catch syntax slips before they reach the program that reads it.
YAML: readable, but syntax-sensitive
service: api
port: 8080
features:
auth: true
cache: false
YAML drops JSON's braces and quotes in favor of indentation, and it's a strict superset of JSON's data model — meaning it can express everything JSON can, plus comments, multi-document files, and anchors/aliases for reusing blocks. That expressiveness is why Kubernetes, GitHub Actions and Docker Compose all standardized on it — these are files engineers read and edit constantly, and the reduced punctuation genuinely helps.
The cost is that YAML's parsing rules are the most complex of the four: indentation is structural (mixing tab and space, or 2- and 4-space blocks in one file, breaks parsing), and bare unquoted words get type-inferred — yes, no, on, off were historically read as booleans in YAML 1.1, a behavior narrowed in YAML 1.2's core schema to just true/false variants, though many parsers still default to the looser 1.1 rules in practice. Converting between JSON and YAML is safe going JSON→YAML; going the other way, watch for anything that looks like it could resolve as a boolean or number when it's meant to stay a string.
TOML: explicit types, built for config specifically
service = "api"
port = 8080
[features]
auth = true
cache = false
TOML's stated goal, per the TOML spec, is to be "a minimal configuration file format that's easy to read due to obvious semantics," designed to "map unambiguously to a hash table." Unlike YAML, TOML does not infer types from bare words — every value has an explicit, unambiguous syntax: strings need quotes, dates use a defined date-time syntax, and there's no equivalent of YAML's bare yes/no ambiguity. It supports comments (#), native date/time types without extra parsing, and — notably — array-of-tables syntax ([[servers]]) for representing repeated structured blocks, which is more explicit than YAML's indentation-based lists for that specific case.
[[servers]]
name = "alpha"
ip = "10.0.0.1"
[[servers]]
name = "beta"
ip = "10.0.0.2"
This is why TOML is the config format for Cargo (Rust), Poetry and modern pyproject.toml (Python), and Hugo — ecosystems that wanted YAML's readability without YAML's implicit-typing footguns. It's a strong fit for flat-to-moderately-nested app config with a fixed set of typed values. It's a weaker fit for deeply nested or highly dynamic structures, where the [table.path] header syntax becomes harder to scan than YAML's indentation. Use the JSON to TOML and TOML to JSON converters when moving config between a TOML-based tool (Cargo, Poetry) and anything that consumes JSON.
INI: the oldest, simplest, and least standardized
[service]
name = api
port = 8080
[features]
auth = true
cache = false
INI predates all three other formats and, unlike them, has no single authoritative specification — different parsers (Python's configparser, Windows .ini handling, PHP's parse_ini_file) disagree on details like whether values are typed at all (INI is often purely string-based, leaving "8080" as text unless the reading program parses it as a number itself), how nesting works (usually: it doesn't, beyond one level of [section]), whether ; or # starts a comment, and how duplicate keys are handled. That lack of a shared spec is exactly why INI doesn't appear as a conversion target here — there's no single "correct" INI to convert to, unlike JSON, YAML and TOML, which all have a documented grammar a converter can target unambiguously.
INI still shows up in practice — Git's .gitconfig, PHP's php.ini, Windows application settings — mostly for legacy reasons or because the config genuinely is flat, two-level, and simple enough that INI's limitations don't matter.
Picking one
- Config a program writes and another program reads, with no human editing in between: JSON. Strict and unambiguous is a feature here, not a downside.
- Config engineers read and hand-edit constantly, often nested (CI pipelines, orchestration manifests): YAML, with disciplined quoting of anything boolean-or-number-looking that's meant to be a string.
- App or tool config with a fixed, mostly-flat set of typed values, where you want to avoid YAML's implicit-typing bugs: TOML.
- A small number of flat key-value settings, especially where an existing tool already expects INI: INI, accepting that portability across parsers isn't guaranteed.
None of these choices is permanent — converting between JSON, YAML and TOML with the tools above is mechanical as long as the source data doesn't rely on a feature the target format lacks (YAML anchors, TOML's native date type, or comments in any format going to JSON).