TOML vs YAML vs JSON: how each one fails you
Every comparison lists the syntax differences. The useful comparison is what each format does when you get something wrong — because that is what you will actually spend your afternoon on.
Most comparisons of these three show you the same object written three ways, note that YAML has less punctuation, and stop. That is not the part that costs you time. What costs you time is the afternoon you spend finding out that your version number became 1.2, or that the second copy of a key silently won.
So this is a comparison by failure mode. Every claim below has the real output underneath it, and every one is something you can reproduce in a console in about ten seconds. Where the three differ most is not syntax — it is how loudly each one complains.
The same config, three ways
Start with something concrete so the rest has a reference point. A service config with a nested section and a list of records:
{
"name": "subscriber-sync",
"enabled": true,
"network": { "mcc": 234, "mnc": 15 },
"sim": [
{ "msisdn": "447700900142", "plan": "Unlimited 5G" },
{ "msisdn": "447700900458", "plan": "Pay As You Go" }
]
}name: subscriber-sync
enabled: true
network:
mcc: 234
mnc: 15
sim:
- msisdn: "447700900142"
plan: Unlimited 5G
- msisdn: "447700900458"
plan: Pay As You Goname = "subscriber-sync"
enabled = true
[network]
mcc = 234
mnc = 15
[[sim]]
msisdn = "447700900142"
plan = "Unlimited 5G"
[[sim]]
msisdn = "447700900458"
plan = "Pay As You Go"YAML is the shortest. TOML is the longest and the flattest — every section says where it belongs, so you can paste a block from the middle of the file into a chat and it still means something. That property matters more than character count when several people edit the same config.
Failure 1: a key written twice
This is the one that comes from merging two configs by hand, and the three formats could not disagree more. Run these:
JSON.parse('{"a":1,"a":2}')
// -> { a: 2 } no error, last one wins
yaml.load('a: 1\na: 2')
// -> YAMLException: duplicated mapping key
toml('a = 1\na = 2')
// -> TomlError: trying to redefine an already defined table or valueJSON accepts it silently. That is not a parser bug — RFC 8259 says names should be unique but leaves the behaviour of duplicates undefined, so each parser picks for itself. Most take the last. It means a config with a stale duplicate ten lines above the real one works fine until someone reorders the file.
YAML and TOML both refuse. If you are choosing a format for something several people edit, that difference is worth more than any amount of syntax elegance.
Failure 2: a number too big to hold
Here is where the three genuinely split, and where the result surprised me enough to be worth writing down. A 19-digit SIM identifier, parsed by the standard library in each format:
JSON.parse('{"iccid":8901240544102066246}').iccid
// -> 8901240544102066000 silently wrong
yaml.load('iccid: 8901240544102066246').iccid
// -> 8901240544102066000 silently wrong
toml('iccid = 8901240544102066246')
// -> TomlError: integer value cannot be represented losslesslyJSON and YAML hand back a different number than the one in your file and say nothing. TOML stops. That is not the parser being fussy — it is the only one of the three whose specification pins integers down: TOML requires 64-bit signed integers to be handled losslessly, so a parser that cannot manage it is obliged to admit that rather than quietly round.
The practical consequence is that TOML files containing long identifiers fail to open in most browser-based tools, because they are all built on the same JavaScript number. Our own TOML to JSON and TOML to Table protect the literal before it reaches the parser, which is why they will open a file that other converters reject.
Failure 3: the value that quietly changes type
All three formats guess types from unquoted text, and all three get version numbers wrong in exactly the same way:
| You wrote | JSON | YAML | TOML |
|---|---|---|---|
1.20 | 1.2 | 1.2 | 1.2 |
2026-01-14 | "2026-01-14" — a string | a Date | a date type |
NO (unquoted) | not valid JSON | "NO" under YAML 1.2 | not valid TOML — quotes required |
007 | 7 | 7 | not valid — leading zeros banned |
The version-number row is the one that bites in practice, and it is unanimous: 1.20 and 1.2 are the same number, so the trailing zero is gone in all three. Quote it and it stays text. This is the single most common cause of a pinned dependency resolving to the wrong release.
The last two rows are more interesting than they look. TOML rejects both — it will not take a bare NO and it bans leading zeros on integers outright — where YAML accepts them and decides what they mean. Strictness is TOML's whole personality, and it is why 007 as an area code is a bug you find at parse time rather than in production.
What each format simply cannot express
Three genuine holes, each of which will decide the choice for you if you happen to need the thing:
| JSON | YAML | TOML | |
|---|---|---|---|
| Comments | None. At all. | Yes | Yes |
| Null | Yes | Yes | No such concept |
| Dates and times | Strings only | Native | Native, with local-vs-offset variants |
| Infinity / NaN | No | Yes (.inf, .nan) | Yes (inf, nan) |
| Comments survive a round trip | Nothing to lose | No — most writers drop them | No — most writers drop them |
| Significant whitespace | No | Yes | No |
The null row is the one people trip over converting into TOML. It is not that null is spelled differently — TOML has no null, so a null-valued key cannot be written at all and has to be dropped, which changes the shape of your document. The issue asking for one has been open for years and is worth skimming if you want to see the argument.
The whitespace row is the reason YAML has a reputation. Indentation carries meaning, tabs are illegal, and a paste from a page that converted tabs is unfixable by eye:
yaml.load('a:\n\tb: 1')
// -> YAMLException: tab characters must not be used in indentation
toml('[t]\n\ta = 1')
// -> { t: { a: 1 } } TOML does not care; indentation is decorationSo which one
Three questions, in order. They resolve most cases without needing an opinion about elegance.
- Is a machine the only reader? Then JSON. It has no comments and no dates, which does not matter if nobody opens it, and every language parses it without a dependency.
- Will people edit it by hand, and is it mostly flat? Then TOML. Sections say where they belong, duplicates are errors, and there is no indentation to get wrong. This is why Rust and modern Python packaging both landed on it.
- Is it deeply nested, or is your ecosystem already YAML? Then YAML. Kubernetes manifests and CI pipelines nest four or five levels deep, and TOML gets ugly fast at that depth — you end up with
[a.b.c.d]headers that are no easier to read than indentation.
The honest summary is that TOML is the best of the three at being edited by a person, YAML is the best at deep nesting, and JSON is the best at not being edited at all. Most arguments about them are really arguments about which of those three situations the arguer is in.
If you just need to move between them
All three describe the same tree, so JSON works as a staging post in both directions — with the caveat that comments are gone the moment you leave YAML or TOML, because JSON has nowhere to put them.
TOML to JSON Opens files other browser tools reject, because long integers are protected before parsing. JSON to TOML Nested objects become sections, arrays of objects become [[blocks]], and null keys are named rather than silently dropped. YAML to JSON The same trip from the YAML side, with the same protection for oversized integers.If the file is a list of records rather than a config, TOML to Table and YAML to Table skip the intermediate step and show it as a grid. And if something will not parse at all, TOML Validator and YAML Validator give you the line and column rather than a stack trace.
The part worth remembering
Forget the syntax comparison — you can read three files and see it. What separates these formats in practice is how they behave when you are wrong.
- JSON is the quietest, and quiet is bad. Duplicate keys resolve silently, and big numbers change without a word.
- YAML complains about duplicates and tabs, and stays quiet about numbers and about types it guessed.
- TOML complains about nearly everything, including things it could have guessed at. That is tiring on day one and the reason a config that parsed still works six months later.
- None of the three protects a version number. Quote it.