The three, side by side

All three encode roughly the same things: maps, lists, strings, numbers, booleans, null. Everything interesting is in what surrounds that.

JSONYAMLTOML
First publishedjson.org, 20021.0, January 20040.1.0, March 2013
Current standardRFC 8259 (2017), ECMA-4041.2 (2009), revision 1.2.2 (2021)1.0.0, January 2021
Commentsnone##
Datesstrings onlytimestamps in the 1.1 schemafirst-class, RFC 3339
Whitespace significantnoyesno
Unquoted stringsneverusually, and that’s the problemnever
Where you meet itAPIs, package.jsonKubernetes, GitHub Actions, AnsibleCargo.toml, pyproject.toml

JSON: discovered, not invented

Douglas Crockford is careful about the wording: he doesn’t claim to have invented JSON, only to have discovered it, because the notation already existed inside JavaScript and he just wrote down the grammar. That grammar went up on json.org in 2002, and it is small enough that he had it printed on the back of business cards. Six types, no versions, no extensions.

Standardisation came later and took a while. RFC 4627 in 2006 was only informational, ECMA-404 landed in 2013, RFC 7159 in 2014, and the current text is RFC 8259 from December 2017, which is also Internet Standard STD 90. The most consequential change in that last revision was mundane: JSON exchanged between systems must be encoded in UTF-8.

One thing RFC 8259 does not do is bound numbers. It sets no limit on range or precision and instead notes that implementations sticking to IEEE 754 doubles are the ones that interoperate. That single sentence is the reason Twitter had to add an id_str field to its API in 2010: tweet IDs passed 2^53 and JavaScript silently rounded them. Anyone shipping 64-bit IDs through JSON is still hitting that today.

The comment hole in JSON

Early JSON had comments. Crockford took them out, and he has said why: "I removed comments from JSON because I saw people were using them to hold parsing directives, a practice which would have destroyed interoperability." Once a comment can change how a document is read, two parsers can disagree about what the same file means, and the whole point of a data-interchange format is gone.

Defensible for data, painful for config, which is exactly why nobody accepted it. The workarounds by popularity: a fake "//" key (npm ignores unknown fields in package.json, so this survives), JSONC with real // and /* */ comments (VS Code settings, tsconfig.json), and JSON5, which also brings back trailing commas, single quotes and unquoted keys. All three are different formats wearing a .json extension, and a strict parser rejects every one of them.

One line settles most of these arguments: if a file needs comments, it should not be JSON. Reaching for JSON5 to fix a format choice usually means the format choice was wrong.

YAML: readable until it isn’t

YAML started in 2001, announced by Clark Evans and developed with Ingy döt Net and Oren Ben-Kiki. The name was originally "Yet Another Markup Language" before it was retired into the recursive "YAML Ain’t Markup Language". Version 1.0 came out in January 2004, 1.1 in 2005, and 1.2 in 2009.

That 1.2 release matters more than its version number suggests. The authors had by then noticed JSON, found it was almost exactly a subset of YAML by coincidence, and rewrote the spec to make JSON a strict subset. In the process they threw out the 1.1 implicit typing rules, the ones that decide an unquoted no is a boolean. The current text is revision 1.2.2, dated October 2021, ten chapters with a full BNF grammar. Set that next to the sixteen pages of RFC 8259 and you have the honest summary of the difference between the two formats.

Here is the catch that trips up half the internet: the 2009 fix is not what your code runs. PyYAML implements the 1.1 rules to this day, its bool resolver regex still matches yes, no, on and off, and libyaml, Ruby’s Psych and older js-yaml behave the same way. Seventeen years after the spec dropped implicit typing, yaml.safe_load still does it. That whole mess deserves its own page, and it has one: the YAML Norway problem.

YAML also carries features no config file asked for. Anchors and aliases (&name and *name) let a document reference itself, which is how you get a billion-laughs attack in a config format: CVE-2019-11253, patched in October 2019, let an authorised user post about a megabyte of nested YAML anchors to the Kubernetes API server and watch it burn CPU and memory expanding them. Tags are the other one. yaml.load in Python could construct arbitrary objects until PyYAML 6.0 made the Loader argument mandatory in 2021.

TOML: the one packaging picked

Tom Preston-Werner, GitHub co-founder, made the first TOML commit in February 2013 and published 0.1.0 that March. The name is a joke at his own expense (Tom’s Obvious, Minimal Language) and the design goal fits in a line: a config file that maps unambiguously to a hash table, with no cleverness in between. Getting from that first commit to a stable 1.0.0 took eight years, landing in January 2021.

Slow, and it paid off. Rust’s Cargo standardised on it, Python chose TOML for pyproject.toml in PEP 518, and Python 3.11 shipped a TOML parser in the standard library as tomllib (read-only, deliberately). Hugo and Netlify use it too. Somewhere between 2016 and 2021 TOML quietly became the default format for language tooling.

What TOML gives you over YAML is the absence of guessing. Every string is quoted, so country = "NO" can never become a boolean. Dates and times are real values in RFC 3339 form, not strings you re-parse later. Whitespace means nothing, so a bad paste can’t silently reparent a key.

Where it gets ugly is depth. Nested tables become bracketed headers like [tool.poetry.dependencies], arrays of tables need the [[bin]] double-bracket form, and past two or three levels the file reads worse than the YAML it replaced. TOML is excellent for shallow config and a bad fit for a deep tree.

Type surprises, ranked

Ranked by how much time they cost people we know:

  1. YAML implicit typing. Under 1.1 rules an unquoted NO is false, 1.10 is the float 1.1, 0777 is the integer 511, and 1:30 is 90 because YAML 1.1 has sexagesimal integers. None of this produces an error. You get a working config with the wrong values.
  2. JSON numbers. One number type, no integer/float distinction in the spec, and a de facto 53-bit safe range because of JavaScript. Send IDs and money as strings.
  3. Duplicate keys. RFC 8259 says names "SHOULD be unique" and leaves the rest undefined, so most JSON parsers take the last one silently. YAML 1.2 calls duplicates an error, but PyYAML happily takes the last one anyway. TOML is the only one of the three that reliably refuses to redefine a key.
  4. Dates. JSON has none, so everyone invented their own convention. YAML 1.1 parses unquoted 2026-07-30 into a date object, which is a surprise if you wanted the string. TOML is the only one where the answer is boring.

Moving config between them

Format migrations are mostly mechanical, with two things worth watching. Comments never survive: a YAML file converted to JSON loses every # line, and there is no fixing that after the fact, so migrate the file once and keep the result. Anchors and aliases get expanded, so a converted document can come out bigger and more repetitive than the original.

The direction that needs the most care is JSON to YAML, because that is where unquoted strings get created. Our JSON to YAML converter writes with the YAML 1.1 schema on purpose: values like NO, 1.10 or 2026-07-30 come out quoted, so they still read as strings in the 1.1-era parsers most projects actually run. Going the other way, YAML to JSON is the fastest way to see what a YAML file really contains, since JSON has no implicit typing to hide behind: if false shows up where you wrote NO, there’s your bug.

Both run entirely in your browser, and for config files that isn’t a marketing line. Config is where the hostnames, connection strings and API tokens live. Pasting one into a form that posts it to someone’s server is a small incident waiting to be discovered later.

Which one to use

Where we have landed, after a decade of arguing about this in pull requests:

  • Data between programs: JSON. Every language parses it, it’s fast, and there is nothing to configure. If a human has to edit it by hand regularly, that’s a design smell, not a reason to switch format.
  • Your own application config: TOML. Comments, no type guessing, real dates, one obvious way to write everything. It’s the format we reach for first for anything new and shallow.
  • Kubernetes, Helm, GitHub Actions, Ansible, docker-compose: YAML. Not a choice, it’s the ecosystem. Quote your strings, add a linter, and treat every unquoted value as a bug waiting to happen.
  • Deeply nested trees: JSON or YAML. TOML’s table headers stop being readable around three levels deep.
  • Anything a machine generates and a human debugs: JSON, formatted. Pretty-printed JSON diffs cleanly and can’t be broken by an editor that eats indentation.

The one line we’d put on a sticker: JSON for data, TOML for config you own, YAML for config someone else designed. And whichever you land on, the failure mode to guard against is the same one every time, which is a value quietly changing type between the file and the program. If you want to see how bad that can get, Norway is the classic example, and Excel does the same thing to CSV files for the same reason.

Choosing between the three

Is YAML better than JSON?

For files humans write and read, yes: YAML has comments, needs no quotes or braces, and survives a diff review much better than a wall of JSON. For data machines exchange, no: YAML’s implicit typing turns NO into false and 1.10 into 1.1, its parsers are slower, and features like anchors and tags have caused real security bugs. The rule of thumb that holds up: JSON for data, YAML only where the ecosystem already forces it.

Is YAML a superset of JSON?

Since YAML 1.2 (2009), almost. The 1.2 spec was written specifically to make every JSON document a valid YAML document, and in practice you can rename a .json file to .yaml and most parsers will read it. People have since found edge cases where the two disagree, and older 1.1 parsers are not a superset at all, so "paste JSON into YAML and it works" is a convenience, not a guarantee you should build a pipeline on.

Why doesn’t JSON support comments?

Douglas Crockford removed them on purpose. His stated reason: "I removed comments from JSON because I saw people were using them to hold parsing directives, a practice which would have destroyed interoperability." Comments existed in early JSON and were cut so that a JSON document could never mean two different things to two different parsers. The common workarounds are a dummy key such as "//", or a dialect like JSONC (what VS Code settings and tsconfig.json use) and JSON5.

Is TOML better than YAML for config files?

For flat or shallow configuration, TOML is the safer choice, because it has no implicit typing: a bare word is a syntax error, not a guess. Strings are quoted, dates are real dates, and there is exactly one way to read every value. TOML gets awkward once your config is a deep tree, since nested tables and arrays of tables turn into long bracketed headers. That is the trade: TOML is duller to write and impossible to misread.

Which is faster to parse, JSON or YAML?

JSON, by a wide margin. A JSON parser handles a grammar small enough to print on a business card, while a YAML parser implements a multi-chapter spec with indentation tracking, anchors, aliases, tags and multiple schemas. In Python the gap is big enough that PyYAML ships a C-backed CSafeLoader for people who hit it, and most tooling that reads YAML at scale (including the Kubernetes API server) converts it to JSON internally before doing real work.

Does JSON allow trailing commas or single quotes?

No to both. RFC 8259 allows only double-quoted strings and forbids a comma after the last element of an array or object, which is why a config file that looks fine in a JavaScript editor gets rejected by a strict parser. JSON5 and JSONC add trailing commas, single quotes and comments back, but they are separate formats: anything that only speaks standard JSON will refuse the file.

Can I use TOML for Kubernetes manifests?

No. The Kubernetes API accepts JSON and YAML only, and YAML manifests are converted to JSON before they are decoded, so the format choice is made for you. The same goes for GitHub Actions workflows, docker-compose files and Ansible playbooks. Where TOML wins is your own application config and language tooling: Cargo.toml, pyproject.toml, Hugo, Netlify.

What is the safest way to convert YAML to JSON?

Locally, with a parser you control, and with a look at the resulting types before you trust the output. Conversion is where implicit typing becomes visible: a value that was an unquoted NO or 1.10 in YAML shows up as false or 1.1 in the JSON, and that is your cue to quote it at the source. Config files usually contain hostnames, tokens or keys, so a converter that runs in your browser instead of uploading the file is the version to pick.