JSON vs YAML vs TOML: Which Configuration Format to Use
A practical comparison of the three most common configuration formats with use cases.
Configuration formats look interchangeable until a parser silently turns no into false, truncates a 64-bit ID, or instantiates an arbitrary object from a config file. JSON, YAML, and TOML dominate modern tooling, and most of the pain teams experience comes from not knowing exactly where each format bends. This is a mechanics-level comparison, not a taste test.
Three Specs, Three Philosophies
JSON is defined by RFC 8259 and ECMA-404: six value types, no comments, and a grammar that fits on a napkin. YAML 1.2.2 (2021) is roughly a hundred pages describing three representation layers, tags, anchors, and multi-document streams. TOML 1.0.0 (2021) sits between them: a typed, comment-friendly format explicitly designed to map unambiguously onto a hash table.
Spec size matters in practice. Every mainstream JSON parser behaves essentially identically. Two YAML parsers can legitimately disagree about the same file, because many libraries (PyYAML included) still implement YAML 1.1 semantics while others implement 1.2.
JSON: Predictable, With Sharp Numeric Edges
{
"port": 8080,
"debug": true,
"hosts": ["a.example.com", "b.example.com"]
}
JSON's real risks hide in numbers and duplicate keys:
- Number precision. RFC 8259 sets no precision requirement, but JavaScript parses every number as an IEEE 754 double. Integers above 2^53 - 1 silently lose digits — the reason Twitter's API ships both
idandid_str. Serialize 64-bit identifiers as strings. - Duplicate keys. The spec only says names SHOULD be unique. Most parsers keep the last occurrence, some keep the first, and that disagreement has powered real security bypasses when a validator and an executor parse the same document differently.
- No comments, by design. Douglas Crockford removed them to prevent comment-based parser directives. For hand-edited config, use JSONC (what
tsconfig.jsonactually is) or JSON5, and be explicit about which one your tooling accepts — plain JSON parsers reject both.
Use JSON for API payloads, lockfiles, and anything machines write more often than humans do.
YAML: Readable, With a 1.1-Shaped Trapdoor
port: 8080
debug: true
hosts:
- a.example.com
- b.example.com
YAML 1.2 made no, yes, on, and off plain strings, but YAML 1.1 parsers coerce them to booleans — the famous Norway problem, where the country code NO becomes false. The 1.1 ruleset also parses 1:30 as the sexagesimal integer 90 and turns the version number 3.10 into the float 3.1. Since you rarely control which parser reads your file, the operational rule is: quote every string that could look like a scalar of another type.
Two more mechanisms deserve respect:
- Anchors and aliases (
&base/*base) deduplicate config nicely but enable the billion-laughs attack: a few nested aliases expand to gigabytes. Parse untrusted YAML with alias limits or reject aliases outright. - Arbitrary object construction. Legacy
yaml.load()in PyYAML could instantiate any Python class from a tagged node. Always usesafe_loador a parser that is safe by default.
Also note that tabs are forbidden for indentation, and a stray space can silently change nesting. If the ecosystem forces YAML on you (Kubernetes, GitHub Actions, Ansible), pair it with schema validation — yamllint plus a JSON Schema from schemastore.org catches most disasters in CI before they reach production.
TOML: Boring on Purpose
port = 8080
debug = true
hosts = ["a.example.com", "b.example.com"]
[database]
url = "postgres://localhost:5432/app"
[[server]]
name = "eu-1"
[[server]]
name = "us-1"
TOML's design goals show up as concrete guarantees:
- Every value is explicitly typed: string, integer, float, boolean, array, table, or an RFC 3339 datetime — dates are first-class, no string convention needed.
- Duplicate keys are a hard parse error, not an implementation choice.
- No indentation semantics: structure comes from
[table]headers and dotted keys, so a reformat can never change meaning.
The cost is nesting ergonomics. Arrays of deeply nested tables get verbose fast, which is why Cargo and pyproject.toml feel great in TOML while a Kubernetes manifest would not. A useful heuristic: if your config nests more than three levels, the problem is the config schema, not the format.
Choosing in Practice
- Machine-to-machine data, APIs, lockfiles: JSON
- Human-edited application or tool config: TOML
- Ecosystem-mandated infrastructure config: YAML, always with schema validation and quoted ambiguous scalars
- Config that needs comments but must stay JSON-compatible: JSONC/JSON5, documented explicitly
Mistakes That Keep Recurring
- Shipping 64-bit IDs as JSON numbers and debugging the corruption weeks later
- Unquoted YAML values: country codes, git SHAs that happen to be all digits, versions like
3.10 - Accepting YAML from users without alias/depth limits
- Assuming every parser honors duplicate-key behavior the same way
- Hand-writing any of these formats via string concatenation instead of a serializer
Converting Between Formats
Because all three share the same data model at the intersection — maps, arrays, scalars — mechanical conversion is usually lossless in one direction only. JSON to YAML always works. YAML to JSON fails on anchors, multiple documents, and non-string keys. TOML to JSON drops the comment layer, which is often the most valuable content in the file. Treat the format humans edit as the source of truth, generate the machine format in CI, and never round-trip: convert one way, commit both artifacts, and diff them in review.
Validate before you commit: paste config into the JSON Formatter at sdk.is/json-formatter or the YAML Validator at sdk.is/yaml-validator and let the parser find the problem first.