Guide
JSON, YAML and XML: picking a format, and their sharp edges
Three formats that all claim to represent "structured data," designed a decade or two apart with different priorities — JSON for machines exchanging data, YAML for humans editing config by hand, XML for documents that need namespaces and schemas. Each design choice bought something and cost something, and most of the surprises show up exactly where you converted from one to another.
What each one actually optimises for
JSON is deliberately minimal — it is close to a strict subset of a JavaScript object literal, with six data types and no way to add your own. That minimalism is the entire point: a JSON parser is small, fast, and has essentially no configuration surface for two implementations to disagree about. The cost is that it has no comments, no trailing commas, and no way to express "this is a date" or "this is a large integer" — every value is a string, a bare number, a bool, null, an object, or an array, and nothing else.
YAML was designed to be pleasant to hand-edit: no mandatory quoting, no braces, indentation instead of delimiters, comments, and a large set of implicit type conversions so you can write true instead of "true". Every one of those conveniences is also a place a config file goes wrong in a way JSON structurally cannot.
XML comes from the SGML document-markup lineage rather than the data-interchange one. It distinguishes elements from attributes, supports namespaces so two vocabularies can mix in one document without colliding, and has a mature schema-validation ecosystem (DTD, XSD, RELAX NG) for enforcing a contract on the data. That is more machinery than most APIs need, which is why JSON displaced it for typical request/response payloads — but it is also why XML is still what you find under the hood of SOAP, RSS/Atom, and file formats like .docx and .xlsx, which really are documents with structure, not just key-value bags.
YAML's implicit typing, and the Norway problem
YAML infers a value's type from how it's written, so an unquoted yes, no, true, false, on, off, y and n (in various cases) all parse as booleans under the older YAML 1.1 rules that most parsers, including js-yaml, still implement by default.
country: NO # Norway's ISO 3166 code -- parses as boolean false
version: 1.10 # trailing zero is dropped -- parses as the number 1.1
id: 0123 # leading zero -- some parsers read this as octal
The first line is the famous "Norway problem": a config or data file listing country codes silently turns Norway into false. The fix is always the same — quote anything that isn't supposed to be auto-typed: country: "NO". YAML 1.2 narrowed the boolean set to just true/false precisely because of bugs like this, but plenty of parsers, including the one behind this converter, still follow the wider 1.1 set for compatibility with existing YAML files, so quoting defensively is still the safer habit.
This converter's YAML → JSON direction preserves whatever js-yaml's type inference decides, which is exactly why an unquoted "NO" arrives on the JSON side as the boolean false rather than the string "NO" — the conversion is doing its job faithfully; the surprise is a property of the source file, not the tool.
Anchors, aliases, and the alias bomb
YAML lets you name a node with &anchor and reuse it elsewhere with *anchor, which is genuinely useful for not repeating a large block in a config file. It also means the parsed size of a document is not bounded by its byte size: an alias can reference another alias, and nesting that a few levels deep multiplies the expansion at every level — the "billion laughs" attack, named after the classic XML version of the same idea.
a: &a ["lol","lol","lol","lol","lol","lol","lol","lol","lol"]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: [*c,*c,*c,*c,*c,*c,*c,*c,*c]
Measured against the exact library this converter uses, 254 bytes of input like the above produced 22 MB of expanded output in about 150 milliseconds — and each additional level of nesting multiplies that again. No input-length limit catches this, because the danger is entirely in the tiny input, not the output. This site's YAML → JSON conversion runs inside a worker thread with a hard heap ceiling and a timeout specifically because of this; a worker that blows either limit is killed on its own rather than taking the request, or the process, down with it. If you're parsing untrusted YAML anywhere yourself, this is the one thing worth building a defence for explicitly — a plain yaml.load() call with no resource limit will happily try to honour the expansion.
The other YAML-specific risk is arbitrary tags: some YAML libraries support !!python/object or !!js/function style tags that reconstruct language-native objects, or execute code, on load. Always use the safe/default loader in whatever library you're using — js-yaml's default load() rejects those tags outright; only the explicitly-named unsafe functions understand them, and there is essentially never a good reason to call one on data you didn't write yourself.
XML's own version of the same problem: external entities
XML has a parallel history to YAML's aliases: a DTD can define an internal entity that expands like a macro, and nested entity definitions produce the same exponential blow-up as YAML's anchors — this is where "billion laughs" was first named, in 2002, well before the YAML version was demonstrated.
XML's more dangerous variant is the external entity: a DTD can define an entity whose value is read from a file path or a URL, and a naive parser will fetch it and splice it into the document — XXE (XML External Entity) injection. Depending on the parser's configuration this can read arbitrary local files, reach internal network services that aren't otherwise exposed, or in the worst case combine with output reflection to exfiltrate data straight out of a supposedly simple document-parsing endpoint.
<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>
This converter validates before formatting rather than accepting-and-repairing malformed input — necessary for a different reason (the underlying formatter library silently rewrites broken markup instead of rejecting it), but validation alone does not imply DTD/entity processing is disabled. If you're parsing XML from an untrusted source in your own code, the fix is explicit: disable DTD processing and external entity resolution in whatever parser you use. Most modern XML libraries default this off already, but it's worth confirming rather than assuming, especially with an older parser or one configured for maximum spec-compliance.
Comments, duplicate keys, and other round-trip hazards
- Comments — YAML and XML both allow them; JSON has none. Converting YAML or XML that relies on comments for documentation into JSON silently drops that documentation, with no error to warn you.
- Duplicate keys — JSON's grammar technically allows
{"a": 1, "a": 2}, and most parsers resolve it by keeping the last value with no warning. YAML behaves the same way. Neither format detects this as an error, so a copy-paste mistake that duplicates a key fails silently rather than loudly. - XML attributes vs. child elements —
<user id="1">Ana</user>and<user><id>1</id><name>Ana</name></user>represent the same information but produce structurally different JSON when converted, and there is no universally agreed mapping — every XML-to-JSON converter makes its own choice about whether an attribute becomes a sibling key, a nested object, or a specially-prefixed field. - Number precision — JSON numbers are commonly parsed into a 64-bit float, which cannot represent every integer above 253 exactly. A large ID that survives a YAML or XML round-trip as text can come out of a JSON parse silently rounded to a neighbouring value.
Picking one
- An API request or response body — JSON, almost without exception. It's what every HTTP client and every mainstream server framework assumes by default, and the format's rigidity is a feature here: less room for two implementations to disagree.
- A config file a person edits by hand — YAML, if the file is small and the team is disciplined about quoting ambiguous scalars; JSON if the file is generated by tooling and only occasionally read by a human, since JSON's lack of comments stops mattering when nobody is meant to hand-edit it and its strictness makes diffs and merges more predictable.
- A document with real structure, mixed vocabularies, or a schema contract that needs enforcing — XML remains the right tool, which is exactly why it's still what you find inside SOAP APIs, RSS/Atom feeds, and Office Open XML files rather than having been fully displaced.
- Anything arriving from outside your own systems — treat parsing itself as the attack surface regardless of format: cap input size, use each library's safe/default loader, and for XML specifically confirm DTD and external-entity processing are off.
Last updated 21 August 2026