Understanding TOML ↔ JSON
The config language that wanted to be obvious.
What TOML adds that JSON deliberately doesn't, and the handful of places where the round-trip isn't quite a no-op.
TOML in one sentence.
Tom's Obvious Minimal Language (Tom Preston-Werner, 2013) is a config-file format that reads as if someone wrote down an INI file and then made sure it was unambiguous. Sections become tables, indented keys become nested objects, and the typing system is richer than JSON's — distinct integers and floats, native dates, multi-line strings. It's the format behind pyproject.toml, Cargo.toml, and most modern Rust and Python tooling.
What TOML has that JSON doesn't.
Three things matter for round-tripping. First, dates and times: TOML has 2026-05-13 as a first-class date value; JSON has only strings. Second, integers vs floats: TOML distinguishes 42 from 42.0 in the value type; JSON's number type doesn't. Third, comments: TOML allows #-prefixed line comments; JSON has none at all. Round-tripping TOML → JSON → TOML loses the comments and may lose the int/float distinction.
What JSON has that TOML doesn't.
The biggest gap is heterogeneous arrays. JSON's [1, "two", true] is fine; TOML allows arrays only of a single type. Converting that JSON to TOML requires inventing an extra layer of typing — usually a wrapper object per element, or a dotted-key flat representation. The other gap is null: JSON has explicit null; TOML has no concept of null and most converters either omit the key entirely or represent it as an empty string with a comment.
A worked round trip.
TOML input: # version pin
[package]
name = "tool"
version = "1.0"
becomes JSON { "package": { "name": "tool", "version": "1.0" } }. The comment drops; the section header becomes nesting. Going back: the JSON re-renders as the same TOML body, minus the original comment line.
Section → nested object
[package] becomes { package: ... }
Each section header introduces one level of nesting.
[package] name="tool" → { "package": { "name": "tool" } }
= Nested JSON object
Array of tables → array of objects
[[products]] becomes [ { ... }, { ... } ]
Double-bracket header signals 'append a table to this array'.
[[products]] name="A" / [[products]] name="B"
= { "products": [ {"name":"A"}, {"name":"B"} ] }
Dotted keys, tables, and inline tables.
TOML lets the same nested structure be written three ways. Dotted keys: servers.alpha.ip = "10.0.0.1". Tables: [servers.alpha] followed by ip = "10.0.0.1". Inline tables: servers = { alpha = { ip = "10.0.0.1" } }. All three produce the same JSON. Which form to use is a style choice; longer config files lean on tables for readability, embedded configs lean on inline tables for compactness.
Datetimes — four kinds.
TOML defines four datetime types: offset date-time (with timezone), local date-time (no timezone), local date, and local time. JSON has none of these as native types — a converter must serialise them to strings. The conventional choice is RFC 3339 strings for all four; the consumer is expected to re-parse them. Round-tripping back, the inferrer has to look at string format to recover the original type, which is mostly but not perfectly reliable.
When to pick TOML over JSON.
TOML is right for files humans will hand-edit — configuration, package metadata, build settings. JSON is right for files programs will produce and consume — API responses, serialisation, log entries. The crossover case is configuration that's both human- edited and programmatically generated; for that, TOML wins on the editing side and loses on the schema-tooling side (JSON Schema has near-universal validator support; TOML's schema story is younger).