TOML vs JSON vs YAML: Which Config Format Should You Use?
Here is the short version of TOML vs JSON vs YAML: JSON is the machine-interchange default that every language parses, YAML is the human-friendly format that runs Kubernetes and CI pipelines, and TOML is the explicit, typed format built for tool and application config like Cargo.toml and pyproject.toml. No single format wins everywhere. The right config file format depends on three things: who edits it, whether you need comments, and how strict the types have to be.
A quick rule of thumb. If a machine writes it and a machine reads it, reach for JSON. If a person hand-edits deeply nested infrastructure every day, YAML earns its complexity. If you want obvious, typed configuration that is hard to fumble, TOML is the safest pick. These formats are not really competitors. They occupy different lanes: JSON for machines, YAML for DevOps, TOML for tooling config. The rest of this guide backs that up with side-by-side examples, the differences that actually cause bugs, and a decision matrix you can apply to your next project.
The 30-Second Answer
If you only have half a minute, this table is the config file format comparison you came for.
| JSON | YAML | TOML | |
|---|---|---|---|
| Comments | No | Yes (#) | Yes (#) |
| Data types | Explicit, minimal | Implicit (inferred from shape) | Explicit, rich |
| Native dates | No | Version-dependent | Yes (four types) |
| Nesting style | Braces {} | Indentation | [section] headers |
| Indentation-sensitive | No | Yes (tabs banned) | No |
| Multi-line strings | No (only \n) | Yes (| and >) | Yes (""") |
| Trailing commas | Forbidden | N/A | Allowed in arrays |
| Superset of JSON | — | Yes (1.2) | No |
| Best for | APIs, data exchange | K8s, CI, Ansible | Rust/Python tooling config |
Three one-line verdicts follow from that table:
- Pick JSON when the file is produced and consumed by programs: API payloads,
package.json,tsconfig.json, anything crossing a system boundary. - Pick YAML when humans hand-edit deep, nested configuration and the ecosystem already expects it: Kubernetes manifests, GitHub Actions, Docker Compose, Ansible playbooks.
- Pick TOML when you want a clear, typed config for a tool or application, mostly flat with a few sections, edited by contributors of varying skill:
Cargo.toml,pyproject.toml, Hugo, Netlify.
Those verdicts hold for concrete reasons, and the reasons are where most config bugs come from.
The Same Config, Three Ways
The fastest way to see the difference is to write one configuration three times. Here is a small service config: a name and version, a couple of top-level flags, a list of features, and a nested database block.
JSON:
{
"name": "acme-api",
"version": "2.4.0",
"debug": false,
"released": "2026-07-02",
"features": ["auth", "metrics", "tracing"],
"database": {
"host": "db.internal",
"port": 5432,
"max_connections": 100
}
}
YAML:
name: acme-api
version: "2.4.0"
debug: false
released: "2026-07-02"
features:
- auth
- metrics
- tracing
database:
host: db.internal
port: 5432
max_connections: 100
TOML:
name = "acme-api"
version = "2.4.0"
debug = false
released = "2026-07-02"
features = ["auth", "metrics", "tracing"]
[database]
host = "db.internal"
port = 5432
max_connections = 100
All three describe the identical data; the difference is what your eye trips on. JSON carries the most punctuation: every key is double-quoted, every level adds braces, and one stray trailing comma is a syntax error. YAML strips almost all of that, so the structure comes from indentation alone, which reads cleanly until a tab sneaks in. TOML sits in between: quotes only on strings, key = value pairs, and a [database] header that names the nested block the way an old INI file would. Notice the released field is a quoted string in all three so the data stays identical here. That is deliberate: dates are exactly where these formats stop agreeing.
JSON — The Machine-Interchange Default
JSON is the format you reach for without thinking, and usually that instinct is right. Every mainstream language ships a parser, every HTTP API speaks it, and its grammar is small enough to hold in your head. It is strict and unambiguous: a string is a string, 5432 is a number, true is a boolean, and there is exactly one way to write each. That strictness is what makes JSON safe to send between systems that have never met. When a Node service posts JSON to a Go service, both agree on the meaning byte for byte.
That same rigidity is why JSON dominates its home turf. package.json, tsconfig.json, and composer.json are JSON because tools generate and rewrite them constantly, and machine-written config wants a machine-strict format. For API payloads and message queues there is simply no reason to choose anything else.
JSON’s weaknesses all show up the moment a human has to edit it by hand. It has no comments. It forbids trailing commas, so reordering a list means fixing commas. It has no multi-line strings, only \n escapes. Every key needs double quotes, and there is no date type, so 2026-07-02 has to live as a quoted string and be parsed by convention. For a config file that a person maintains, those gaps add up to friction.
The “JSON has no comments” problem
The single most requested JSON feature is the one Douglas Crockford deliberately left out: comments. His reasoning was that comments invited people to smuggle parsing directives into config files, breaking interoperability. Whatever you think of that call, you cannot annotate a plain JSON config, which is painful for anything humans maintain. The practical fix is a superset. JSON5 adds comments, trailing commas, unquoted keys, and single quotes; JSONC (JSON with Comments) is the lighter variant that VS Code uses for its settings. Both keep JSON’s shape while making it editable. If you need comments but want to stay in the JSON family, read our JSON5 and JSONC formatting guide for when to use each and how to keep tooling happy.
YAML — The Human-Friendly DevOps Standard
YAML optimizes for the human at the keyboard. It supports comments, its indentation-based layout keeps deeply nested structures clean, and anchors let you define a block once and reuse it with an alias instead of copy-pasting:
defaults: &defaults
timeout: 30
retries: 3
staging:
<<: *defaults
host: staging.internal
production:
<<: *defaults
host: prod.internal
The &defaults anchor names a block, and <<: *defaults merges it into both environments, so a change to the shared timeout happens in one place. Neither JSON nor TOML has anything like it. That readability is why YAML became the lingua franca of operations: Kubernetes manifests, GitHub Actions workflows, Docker Compose files, and Ansible playbooks are all YAML. When your config is five levels deep and a human tweaks it daily, YAML’s low punctuation is a genuine relief.
The cost is complexity and surprise. The YAML 1.2 specification is large, and full compliance is rare, so different parsers disagree at the edges. Indentation is significant, tabs are forbidden outright, and a misaligned space can change meaning or fail to parse. The sharpest edge is implicit typing: YAML guesses the type of an unquoted scalar from its shape, and it guesses wrong often enough to have a nickname.
The Norway problem in one paragraph
Write country: NO in YAML and many parsers hand you the boolean false, not the string "NO", because YAML 1.1, which most Kubernetes tooling still follows, treats NO, YES, ON, OFF, Y, and N as booleans. Norway’s country code becomes false, silently, with no error. The same implicit typing turns 0755 into an octal number and 1.20 into 1.2. The fix is boring but reliable: quote strings that could be mistaken for something else. This footgun is famous enough to have its own article, covering real outages and the YAML 1.1-versus-1.2 history, in our YAML Norway problem guide. For this article, the takeaway is simple: YAML’s convenience and its danger come from the same feature.
YAML wins when people edit deeply nested config every day and the surrounding ecosystem already demands it. If you are writing a Helm chart, you are writing YAML, and that is fine.
TOML — Explicit Config Built for Tooling
TOML (Tom’s Obvious, Minimal Language, created by Tom Preston-Werner) was designed to be the config format YAML users wished they had: readable, but without the guessing. Its defining trait is explicitness. Types are unambiguous, so port = 5432 is always an integer and name = "acme" is always a string, with no shape-based inference to trip over. TOML syntax borrows the [section] header from INI files, which makes the top level of a config scannable, and it is not indentation-sensitive, so whitespace never changes meaning. The TOML 1.0.0 specification is small and stable, which is a feature: there is less to misremember.
TOML’s headline capability is native date and time types, which neither JSON nor plain YAML handle cleanly. It has four of them: offset date-time with a timezone (1979-05-27T07:32:00Z), local date-time without one, local date (1979-05-27, just a calendar day), and local time (07:32:00). That means a deployment date stays a real date instead of a string you have to reparse.
Three pieces of TOML syntax do most of the work. A [section] header opens a table; a dotted header like [tool.ruff] nests one table inside another without extra indentation; and an inline table { version = "1.0", features = ["derive"] } packs a small object onto one line. Repeated sections use a doubled header, the array of tables:
[[servers]]
name = "alpha"
ip = "10.0.0.1"
[[servers]]
name = "beta"
ip = "10.0.0.2"
That block becomes a JSON array of two objects under the key servers. It reads well for a flat list, which is exactly what most tool config needs.
The weaknesses are real but narrow. TOML has no null type, so an absent value is simply an omitted key. Deep nesting gets verbose, because every level of a repeated nested structure repeats the full [[path]] header, which grows noisier than the YAML equivalent. And TOML is younger and less universal than JSON or YAML, so not every tool speaks it. TOML shines for configuration that is mostly flat with a handful of labeled sections, which describes the overwhelming majority of tool config.
Why TOML for Rust and Python?
TOML’s rise tracks two ecosystems that adopted it as a standard. Rust’s package manager, Cargo, uses Cargo.toml for every crate, so every Rust developer reads and writes TOML from day one. Python followed: PEP 518 introduced pyproject.toml to declare build requirements, and PEP 621 standardized project metadata (name, version, dependencies, and [tool.*] sections) in that same file, replacing a scatter of setup.py, setup.cfg, and per-tool configs. Beyond those two, Hugo, Netlify, Poetry, and Foundry all default to TOML. The common thread is tooling config that contributors edit by hand and that benefits from being obvious rather than clever.
Head-to-Head — The Differences That Actually Bite
The comparison table above smooths over the messy parts. These are the specific differences that cause real problems, each with a small example you can run through any parser.
Comments
JSON has no comment syntax at all. TOML and YAML both use # to end-of-line.
# TOML: a leading comment
port = 5432 # and a trailing one
# YAML: identical comment style
port: 5432 # trailing comment
The moment you paste a // or # into strict JSON, parsing fails. This is not cosmetic. A config a human maintains without comments accumulates tribal knowledge that lives only in someone’s head.
Data types and dates
JSON’s types are explicit but sparse, with no date type. TOML’s types are explicit and rich. YAML’s are implicit and, as covered above, occasionally wrong. Dates are the clearest divide. TOML expresses all four kinds natively:
odt = 1979-05-27T07:32:00Z # offset date-time (has timezone)
ldt = 1979-05-27T07:32:00 # local date-time (no timezone)
ld = 1979-05-27 # local date (a calendar day)
lt = 07:32:00 # local time
Convert that TOML to JSON and each value becomes a string that keeps its meaning: a local date stays "1979-05-27" rather than being inflated into a fake midnight timestamp. Our TOML to JSON converter preserves that date kind exactly, which many naive converters get wrong. YAML, meanwhile, may or may not treat 1979-05-27 as a date depending on the parser and schema version, which is precisely the ambiguity you do not want in production config.
Nesting depth
The deeper your config nests, the more the formats diverge. Take a small Docker-Compose-style tree:
services:
web:
build:
context: .
args:
NODE_ENV: production
ports:
- "8080:8080"
YAML expresses depth with pure indentation, and it stays compact. The same structure in TOML forces you to spell out the full path in each header, and to move scalar values ahead of child tables:
[services.web]
ports = ["8080:8080"]
[services.web.build]
context = "."
[services.web.build.args]
NODE_ENV = "production"
JSON carries the tree in nested braces, which is unambiguous but punctuation-heavy. For shallow config the three feel similar; past three or four levels, YAML is visibly leaner and TOML visibly more repetitive. That single difference explains why Kubernetes chose YAML and Cargo chose TOML.
Indentation sensitivity
Only YAML cares about whitespace. In JSON and TOML you can indent however you like, or not at all, and the meaning is identical. In YAML, indentation is the structure, and tabs are illegal, so an editor that inserts a tab or a block pasted at the wrong depth silently changes the document or refuses to load. A list item indented one space too far can attach itself to the wrong parent key without any error, which is a maddening bug to spot by eye because the file still looks reasonable. If your contributors use mixed editors and settings, that is a standing tax YAML charges and the other two do not, and it is a strong reason to run a linter in CI on any large YAML config.
Multi-line strings
JSON cannot hold a literal newline inside a string; you escape it as \n. YAML and TOML both have real multi-line strings.
{ "note": "line one\nline two" }
note: |
line one
line two
note = """
line one
line two
"""
YAML’s | keeps newlines (a > block folds them into spaces instead), and TOML’s """ behaves similarly, trimming the newline that immediately follows the opening quotes. For embedded scripts, SQL, or help text, this alone can decide the format.
Trailing commas and strictness
JSON is the strictest of the three. A trailing comma after the last array element is a syntax error:
# valid TOML — the trailing comma is fine
ports = [
8001,
8002,
]
The same trailing comma after 8002 in JSON fails to parse, which is why adding a line to a JSON array so often means also fixing the comma on the line above. TOML allows the trailing comma, and YAML’s dash-per-line list syntax sidesteps the question entirely. Strictness is a virtue for machine interchange and an annoyance for hand editing, which is the same tradeoff seen from the other side. This is also the friction JSON5 and JSONC set out to remove for JSON specifically.
Big integers and the 2^53 ceiling
All three formats can write a 64-bit integer as text. The trouble starts when that value passes through JavaScript, because browser JSON numbers are IEEE-754 doubles and can only represent integers exactly up to 2^53 − 1 (9007199254740991). A Snowflake ID or a nanosecond timestamp is larger than that, so a round-trip through a JS-based tool rounds it and corrupts the value:
snowflake = 1420070400000000000 # exact in TOML text
{ "snowflake": "1420070400000000000" }
The reliable fix in every format is to store such values as quoted strings, so no numeric parser ever touches them. This is not a flaw in any one format; it is a limitation of the runtime doing the conversion. Our converters warn you when an integer crosses that line instead of corrupting it quietly.
How to Choose — Decision Matrix & Flowchart
When you are actually deciding which config format to use, walk the questions in order and stop at the first strong yes.
- Is the file written and read mainly by programs, or does the ecosystem already mandate a format? If yes, use JSON. An API payload is JSON. A file next to
package.jsonis JSON. Do not fight the tooling. - Do humans hand-edit deep, nested configuration daily, inside a stack that expects it (Kubernetes, CI, Ansible)? If yes, use YAML, and adopt a quoting convention to dodge the Norway problem.
- Is it tool or application config, mostly flat with a few sections, edited by contributors of varying skill, where typed and obvious beats terse? If yes, use TOML.
If two answers tie, break it on this axis map:
| Requirement | Best fit | Why |
|---|---|---|
| Comments in the file | TOML or YAML | JSON has none |
| Native date/time values | TOML | Four explicit date types |
| Very deep nesting | YAML | Indentation stays compact |
| Zero whitespace surprises | JSON or TOML | Not indentation-sensitive |
| Locked into an ecosystem file | JSON | package.json, tsconfig.json |
| Human edits, few sections | TOML | Explicit types, INI-like headers |
| Machine-to-machine exchange | JSON | Universal, strict, fast |
| Type inference must never guess | JSON or TOML | YAML infers implicitly |
Most projects end up using more than one. A repository might have a JSON package.json it never touches by hand, a TOML pyproject.toml for its Python tooling, YAML workflows in .github, and a .env file for local secrets that follows none of these grammars at all. That mix is normal, and it is the point: each file lives in the format that suits its editor. If the .env layer is part of your setup, our .env file format guide covers where it fits alongside JSON config and how to convert between the two.
Converting Between TOML, JSON, and YAML
You end up converting between these formats more often than you would expect. A CI job reads a hand-written pyproject.toml and needs it as JSON to feed a dependency dashboard. A migration moves an app from a YAML config to TOML for stricter typing. A code review is easier when you diff the JSON form of two files rather than their whitespace-sensitive originals. Each direction has a gotcha worth knowing before you paste.
TOML to JSON. The main risk is dates. A local date like 1979-05-27 should stay a calendar date, not become a midnight UTC timestamp that invents a time and timezone. Comments are dropped too, since JSON cannot hold them. The TOML to JSON converter preserves each date kind faithfully so round-trips stay lossless.
JSON to TOML. TOML is stricter about structure, so two things can block a conversion. The top level must be an object, because a TOML document is always a table at its root; a bare array or scalar has nowhere to go. And TOML has no null, so a null object value gets dropped (with a warning listing which keys), while a null inside an array has no valid TOML representation at all. The JSON to TOML converter surfaces both cases explicitly instead of producing broken output.
JSON and YAML, both directions. Here the Norway problem is the thing to watch: an unquoted NO or 0755 can flip type as it crosses the boundary. The JSON to YAML converter and YAML to JSON converter handle the quoting so a string stays a string.
After any conversion, it is worth validating the result. Running the output through the JSON Formatter confirms the JSON is well-formed and consistently indented before you commit it or send it downstream. And because config files carry secrets (registry tokens, database credentials, deploy keys), all of these run entirely in your browser: nothing is uploaded, so a Cargo.toml with a private registry token or a values file with credentials never leaves your machine.
FAQ
Is TOML better than YAML?
Neither is universally better; the TOML vs YAML choice comes down to shape. TOML is harder to get wrong thanks to explicit types and no indentation traps, so it wins for mostly-flat tooling config. YAML expresses deep nesting more compactly, so it wins for large layered infrastructure like Kubernetes.
Should I use JSON or YAML for config files?
For config a human edits, prefer YAML: it allows comments and reads cleanly, which JSON does not. For config that programs generate and consume, prefer JSON: it is strict, fast, and universal. The dividing line is who does the editing, human or machine.
Why doesn’t JSON allow comments?
JSON’s creator, Douglas Crockford, removed comments on purpose. He worried people would embed parsing directives in them and break interoperability between parsers. If you need comments in a JSON-shaped file, use JSON5 or JSONC, which add them back without changing the core structure.
What is the Norway problem in YAML?
It is YAML 1.1 treating certain unquoted words as booleans, so country: NO becomes false instead of the string "NO". YES, ON, and OFF behave the same way. Quoting the value prevents it; see the dedicated guide linked above for the full story.
Does TOML support comments?
Yes. TOML uses # for comments, both on their own line and trailing a value, exactly like YAML. Note that comments are lost when you convert TOML to JSON, since JSON has no comment syntax to hold them.
Can TOML represent everything JSON can?
Almost. The JSON vs TOML gap is small but real: TOML has no null type, so JSON nulls are dropped or rejected on conversion, and a TOML document must be a table at the top level, so a bare JSON array or scalar cannot convert without being wrapped under a key first.
Is YAML a superset of JSON?
Since YAML 1.2, yes: any valid JSON document is also valid YAML, so a YAML parser can read JSON directly. It is a useful fact, but do not lean on it as a security boundary, because YAML parsers add features (like anchors) that JSON does not have.
Which config format is fastest to parse?
JSON is generally fastest, with the most mature and optimized parsers in every language. YAML is the slowest and most complex to parse because of its large specification and implicit typing. TOML sits in the middle, closer to JSON in speed than to YAML.