Every project eventually needs a config file, and the choice between JSON, YAML, and TOML is rarely made deliberately. Usually you inherit whatever the framework picked. But each format makes real trade-offs, and a few of them cause bugs that are genuinely hard to debug.

This is a comparison from the perspective of someone who has to write, read, and parse these files in production — not a spec walkthrough.

The same config in all three

Start with a concrete example. Here's a small server config expressed three ways.

JSON:

json
{ "name": "api-gateway", "port": 8080, "debug": false, "hosts": ["a.internal", "b.internal"], "limits": { "timeout_ms": 5000, "max_body_mb": 10 } }

YAML:

yaml
name: api-gateway port: 8080 debug: false hosts: - a.internal - b.internal limits: timeout_ms: 5000 max_body_mb: 10

TOML:

toml
name = "api-gateway" port = 8080 debug = false hosts = ["a.internal", "b.internal"] [limits] timeout_ms = 5000 max_body_mb = 10

All three express the same data. The differences only matter as files grow, as humans edit them, and as machines parse untrusted input.

Readability

Readability isn't one property — it splits into writing and reviewing.

For a human typing a config by hand, YAML reads cleanest for nested data. No braces, no quotes on most keys and strings, indentation that mirrors the structure. This is why Kubernetes, GitHub Actions, and Ansible all landed on it.

For a human reviewing a diff, TOML and JSON win. YAML's reliance on indentation means a change three levels deep can look identical to a change at the top level in a diff, and a stray space changes meaning silently. TOML's flat

text
key = value
lines and explicit
text
[section]
headers make diffs unambiguous. JSON diffs are noisy with braces but never structurally ambiguous.

For deeply nested data, JSON and YAML scale; TOML does not. TOML was designed for flat-to-moderately-nested config, and once you're three or four levels deep its

text
[a.b.c]
table syntax and array-of-tables
text
[[a.b]]
notation get awkward. If your data is a tree, that's a signal to reach for YAML or JSON.

Comments

This is often the deciding factor and it's a short section because the answer is blunt.

  • JSON has no comments. None. The spec doesn't allow them. You can fake it with a
    text
    "_comment"
    key, but that pollutes your data, and any strict parser or schema validation will choke or ignore it.
  • YAML supports comments with
    text
    #
    .
  • TOML supports comments with
    text
    #
    .

For a file that humans maintain — explaining why a timeout is 5000ms, or flagging a value as environment-specific — the lack of comments makes raw JSON a poor hand-edited config format. This single limitation is why

text
.json
config files often migrate to YAML or TOML, or to JSONC (JSON with comments), a non-standard superset used by editors like VS Code.

If you're working with JSON and need to reshape or validate it, a JSON formatter will at least keep the structure clean, but it won't give you comments — that's a format limitation, not a tooling gap.

Data types

What the format can represent natively matters more than people expect.

JSON has a small, well-defined type set: string, number, boolean, null, array, object. Numbers don't distinguish int from float (they're all "number"), and there's no native date type. This minimalism is a feature — there's exactly one way to read a JSON value, which is why it's the lingua franca of APIs.

YAML is a superset of JSON and adds more: it has explicit null (

text
null
,
text
~
, or empty), multi-line strings via
text
|
and
text
>
, anchors and aliases for reuse (
text
&anchor
/
text
*alias
), and merge keys. This power is also where YAML's footguns live (more below).

TOML has the richest unambiguous type set for config: strings, integers, floats, booleans, first-class dates and times (offset datetime, local datetime, local date, local time), arrays, and tables. The date support is genuinely useful and neither JSON nor YAML matches it cleanly.

toml
released = 2026-06-02T09:30:00Z window = 09:00:00

If your config carries timestamps, TOML handles them with no special casing.

Failure modes

This is where the real differences show up. A format's failure modes determine how much time you lose to mysterious bugs.

YAML: significant whitespace

YAML uses indentation for structure, and it must be spaces, never tabs. Mix them and you get a parse error — if you're lucky. If you're unlucky, the indentation is valid but wrong, and your nested key silently becomes a sibling instead of a child. The file parses, the program runs, and the value just isn't where you think it is.

yaml
limits: timeout_ms: 5000 max_body_mb: 10 # one space short — now a sibling of 'limits', not a child

This class of bug is hard to spot by eye and produces no error. Validating YAML structure before deploy is worth the habit; pasting it into a YAML formatter to see how the parser actually interprets the nesting will surface a misplaced key faster than rereading the raw file.

YAML: the Norway problem

The most infamous YAML gotcha. In the YAML 1.1 spec — which many widely used parsers still follow — these unquoted values are interpreted as booleans:

yaml
countries: - NO # Norway? No — this becomes the boolean false - SE - FR

text
NO
is read as
text
false
. So are
text
yes
,
text
no
,
text
on
,
text
off
,
text
true
,
text
false
,
text
y
, and
text
n
in various casings. Norway's ISO country code is
text
NO
, hence the name. The fix is to quote strings that could be misread:
text
"NO"
. The deeper lesson is that YAML's implicit type coercion is aggressive —
text
1.0
is a float,
text
1_000
may be a number,
text
version: 1.10
can lose its trailing zero and become
text
1.1
. When in doubt, quote.

YAML 1.2 narrowed boolean coercion to only

text
true
/
text
false
, but you can't assume which version your parser implements, so quoting remains the safe practice.

JSON: trailing commas and strictness

JSON's failure mode is the opposite — it's too strict for hand editing. A trailing comma after the last array element or object key is a syntax error. Comments are an error. A single missing quote breaks the whole parse. These are good properties for machine-to-machine data and bad ones for a file a human edits under deadline pressure. The error messages are usually precise (line and column), which softens the blow.

TOML: verbosity and nesting limits

TOML's failure modes are mild by comparison. The main one is that nested structures get verbose and the

text
[[array.of.tables]]
syntax confuses people who haven't seen it. There's no whitespace sensitivity and no implicit type coercion of the Norway variety — a string is a string only if quoted, which removes a whole category of ambiguity. The cost is that everything must be quoted and explicit, which feels heavy for simple files.

Tooling and ecosystem

  • JSON has universal support. Every language parses it in the standard library, every editor highlights it, and it's the native format for REST and most web tooling. Schema validation via JSON Schema is mature and widely adopted.
  • YAML has strong tooling but inconsistent tooling — different parsers implement different spec versions, which is part of why the Norway problem persists. JSON Schema can validate YAML too, since YAML is a JSON superset.
  • TOML has good and growing support. It's the standard for Rust (
    text
    Cargo.toml
    ) and modern Python packaging (
    text
    pyproject.toml
    ), and parsers exist for every major language, but it's less ubiquitous than the other two and has no equivalent to the JSON Schema ecosystem yet.

When to use each

Here's the guidance, stated plainly.

Use JSON when:

  • The data is produced and consumed by machines (API payloads, build artifacts, lockfiles).
  • You need a format every language reads without a dependency.
  • Humans rarely hand-edit it, so the lack of comments doesn't hurt.

Use YAML when:

  • Humans write and read deeply nested config by hand (CI pipelines, Kubernetes manifests, infrastructure).
  • You value readability of the written form over diff clarity.
  • You're prepared to quote ambiguous strings and validate indentation — and ideally enforce that in CI.

Use TOML when:

  • The config is flat to moderately nested (application settings, tool configuration, package metadata).
  • You want comments and unambiguous parsing with no whitespace traps.
  • You have dates or want clean section grouping that reads well in diffs.

A practical rule

If a machine writes it, JSON. If a human writes a tree, YAML — with quoting discipline. If a human writes settings, TOML. Most projects end up using more than one, and that's fine: a

text
package.json
lockfile, a
text
.github/workflows
YAML pipeline, and a
text
pyproject.toml
can all live in the same repo, each playing to its strengths.

The formats aren't competing for one job. They're tuned for different ones, and matching the format to the editor — human or machine — is what keeps config maintainable.