What each format is actually optimised for
| Format | Strength | Weakness | Typical use |
|---|---|---|---|
| JSON | Unambiguous, universally supported, strict | No comments, no trailing commas, verbose | APIs, data interchange, machine-to-machine |
| YAML | Readable, comments, anchors for reuse | Type inference surprises, whitespace-sensitive | Human-edited config, CI pipelines |
| TOML | Explicit types, comments, obvious structure | Awkward for deeply nested data | Application and package config |
They overlap enough to look interchangeable, and the overlap is where the bugs are.
YAML's type inference is the famous trap — and it depends on the version
YAML infers types from how a value looks, and the two major versions infer differently. This is the single most confusing thing about YAML config, because the same file can parse two ways in two programs. The table below is the actual behaviour of the parser on this site, which follows YAML 1.2:
| Written | YAML 1.2 (this site) | YAML 1.1 (older parsers) |
|---|---|---|
country: NO | "NO" — a string | false — a boolean |
version: 1.10 | 1.1 — a number | 1.1 — a number |
perm: 0755 | 755 — decimal | 493 — octal |
flag: on | "on" — a string | true — a boolean |
ratio: 1:30 | "1:30" — a string | 90 — sexagesimal |
So the notorious “NO became false” outage is a YAML 1.1
problem. If your parser is 1.1 (older PyYAML, some Ruby and Java libraries, many CI systems), a country
code, a two-letter state abbreviation, or a product code written unquoted silently becomes a boolean.
Under 1.2 it stays a string — which is why a config can work locally and break in CI.
The durable rule is unaffected by which version you are on: quote anything that must be a string. Version-specific inference is a trap you avoid by not relying on it.
country: "NO" # always a string, both versions
perm: "0755" # always a string; parse it yourself if you need octal
version: "1.10" # keeps the trailing zero, which a number would drop
Note version: 1.10 in the table: even under 1.2 it becomes the number 1.1,
losing the trailing zero. If the value is a version, it is a string, and it needs quotes.
Octal is its own trap
Under 1.2, 0755 is the decimal number 755 — not the octal permission 493. That is
arguably a better default, but it means a file-mode value written unquoted is wrong in a way
that looks right in the text. Write file modes as strings ("0755") and convert explicitly.
What a conversion loses
| From → To | What survives | What does not |
|---|---|---|
| JSON → YAML | All data, types | Nothing structurally — but the output may become implicitly typed if re-read carelessly |
| YAML → JSON | Data, types | Comments, anchors/aliases (expanded), merge keys, ordering guarantees |
| JSON → TOML | Data, types | Null (TOML has no null), top-level arrays of mixed types |
| TOML → JSON | Data, types | Comments, datetime becomes a string |
| XML → JSON | Elements and text | Attributes, namespaces, mixed content, ordering, comments |
Those losses are the reason “convert then convert back” is not a safe migration strategy. Each arrow is a real translation with lossy corners, not a re-indentation.
The null problem in TOML
TOML has no null value. A JSON object with "timeout": null has no direct TOML
representation, so a converter must either omit the key or write an empty string — and those mean
different things when read back. An omitted key usually means “use the default”; an empty string means
“explicitly empty”. If your config distinguishes them, TOML cannot carry that distinction without a
convention.
Numbers: precision and representation
JSON numbers are parsed by some languages as floats, so a 64-bit integer id can lose precision above 253. YAML and TOML have the same issue depending on the parser. If an id must survive, keep it a string in every representation — this is why large systems return ids as strings.
The same applies to decimals in money. 1.10 is not representable exactly in binary
floating point; storing it as a number invites a rounding drift that appears only after arithmetic.
Store cents as an integer, or the amount as a string.
Comments and ordering
Neither JSON nor its object model has comments, and neither JSON nor TOML preserves key order as meaningful data. YAML does preserve order in practice and supports comments. If your config relies on comments to explain non-obvious choices, converting to JSON for a tool and back will delete them. That is not a bug in the converter; it is the format being asked to carry something it does not have.
Practical guidance
- Use JSON between machines. It is strict, which is a feature when the producer and consumer are different programs.
- Use YAML only for files humans edit, and quote every string that could be misread. Prefer a schema validator to catch the rest.
- Use TOML for application config with shallow nesting. It makes types explicit and keeps comments.
- Never round-trip as a migration. Pick the target format once, convert, and then review the diff — do not assume the reverse conversion reproduces the original.
- Keep ids and money as strings in every format, always.
XML is a different kind of thing
JSON, YAML and TOML are all trees of maps and arrays — converting between them is mostly a formatting exercise with the edge cases above. XML is not: it is a document format with attributes, namespaces, mixed content (text and elements interleaved) and ordered siblings. Mapping it onto a map loses information no matter which convention you pick.
| XML feature | What JSON can hold | What is lost |
|---|---|---|
| Attributes | Keys with a naming convention (@id, _id) | Nothing, if the convention is consistent |
| Repeated siblings | An array — but only if the converter knows they repeat | A single occurrence and a repeated one look different |
| Mixed content | Not representable | Text interleaved with elements collapses or reorders |
| Namespaces | Prefixes in keys, or dropped | The namespace URI is what disambiguates; prefixes are arbitrary |
| Comments, CDATA | Nothing | Dropped |
| Element order | Object order, if preserved | May or may not survive a round trip |
The practical consequence: XML → JSON → XML is not a safe migration for a document format. If the data is genuinely a tree of records (an API response, a config), the conversion is fine. If the XML is a document with mixed content, the conversion will silently reorganise it.
Choosing by where the file lives
| Situation | Format | Reason |
|---|---|---|
| HTTP API request or response | JSON | Every client parses it; strictness prevents ambiguity |
| Hand-edited app config | TOML or YAML | Comments and readable nesting |
| CI pipeline definition | YAML | The ecosystem expects it; quote anything type-ambiguous |
| Package metadata | JSON or TOML | Depends on the ecosystem's convention |
| Document interchange (office, publishing) | XML | It carries structure and mixed content the others cannot |
| Large analytical dataset | CSV or Parquet | Not a nested format at all; columnar wins at scale |
Validation is not the same as parsing
A parser tells you the document is syntactically well-formed. It does not tell you the data is correct.
A YAML file with port: "eighty" parses fine and fails at use. A JSON payload with a missing
required field parses fine. Format validation is necessary and never sufficient; if the value matters,
validate against a schema (JSON Schema, TOML schema, or an application-level
check) before acting on it.
This is also why “it converted without error” is not evidence that a conversion was lossless. Every conversion in the table above will succeed on a document it is silently mangling.
Check it yourself
The data format converter does JSON ↔ YAML ↔ TOML ↔ XML in the browser and shows the converted output directly, so you can see what a real conversion does to your file before committing to it. JSON formatter validates and pretty-prints JSON on its own.