Same data — name: Ada in YAML, {"name":"Ada"} in JSON, <name>Ada</name> in XML — three texts, different trade-offs. This complete comparison explains syntax side-by-side, feature table, when to use each (YAML for human config, JSON for web APIs, XML for documents with namespaces), and how to convert losslessly — no prior format deep dive needed.
- YAML: Human config — indentation,
name: Ada # comment, anchors&, no braces; great for K8s manifests,compose.yml, GitHub Actions; superset of JSON (valid JSON is valid YAML). Explore via YAML tools. - JSON: Web API interchange — strict
{"name":"Ada"}with"quotes"mandatory, no comments, no trailing comma; fastest, native to JS viaJSON.parse. Use for REST,package.json, logs. Explore via JSON tools; convert YAML to JSON via our YAML to JSON converter to feed an API expecting JSON. - XML: Document markup —
<person><name>Ada</name></person>with attributes, namespacesxmlns, schema XSD; verbose but mixed content and legacy (SOAP, RSS, SVG, docx). Explore via XML tools. - Quick pick: Human-edited config with comments → YAML; machine-to-machine strict interchange → JSON; document with mixed content/namespaces/legacy → XML.
- Convert: YAML ↔ JSON lossless (same data model); XML → JSON lossy (attributes
@idconvention, no universal). Always validate after conversion.
What Are YAML, JSON, and XML — One Data, Three Serializations
All three carry the same structure — a person with name and age:
# YAML
person:
name: Ada
age: 30
tags: [js, yaml]
// JSON (RFC 8259)
{
"person": {
"name": "Ada",
"age": 30,
"tags": ["js", "yaml"]
}
}
<person>
<name>Ada</name>
<age>30</age>
<tags><tag>js</tag><tag>yaml</tag></tags>
</person>
YAML (YAML Ain't Markup Language) is human-first — no braces, indentation denotes nesting, # comment, anchors for reuse — per YAML 1.2.2 spec. JSON (RFC 8259 via json.org and RFC 8259) is machine-first — strict, fast, native to JS. XML (W3C XML 1.0) is document-first — mixed content (text <b>bold</b> text), attributes, namespaces.
Standards and History
XML (1998) standardized by W3C with namespaces and XSD; JSON (2001, Douglas Crockford, RFC 8259 2017) won the web for its JS literal subset; YAML (2001) became the config favorite for its readability and as a superset of JSON — every valid JSON is valid YAML, so you can paste {"a":1} into a YAML parser and it works, but not vice versa.
Syntax Side by Side — Indentation vs Braces vs Tags
| Construct | YAML | JSON | XML |
|---|---|---|---|
| Object | person: | {"person":{"name":"Ada"}} | <person><name>Ada</name></person> |
| Array | tags: or [js, yaml] | ["js","yaml"] | <tags><tag>js</tag></tags> |
| String | name: Ada (quotes optional) | "name":"Ada" (quotes mandatory) | <name>Ada</name> |
| Number | age: 30 | "age":30 | <age>30</age> (string, parse) |
| Comment | # comment | No (JSONC/JSON5) | <!-- comment --> |
| Attribute | N/A | N/A — use nested object | <user id="42"> |
Rules: YAML indent 2 spaces (not tabs), # comment full line or after space, anchors defaults: &defaults {timeout: 5} then <<: *defaults to reuse, no braces needed. JSON keys "quoted" mandatory, no trailing comma before } or ], no single quotes, no comments per RFC 8259 (JSONC allows //). XML must close </tag> or <tag/> empty, attributes <tag attr="val"> double-quoted, namespaces <svg xmlns="http://www.w3.org/2000/svg"> prevent collision.
Quoting Nuance
YAML quotes optional: name: Ada and name: "Ada" both → string Ada, but port: "3000" (quoted) → string "3000" vs port: 3000 → number 3000. JSON always quoted: "name":"Ada" must, "port":"3000" is string not number. XML content is always text — <port>3000</port> is string "3000" parse to number yourself.
Feature Table — Human Read vs Machine Strict vs Document Power
| Feature | YAML | JSON | XML |
|---|---|---|---|
| Human readability | ★★★★★ — no braces, clean | ★★★ — braces + quotes | ★★ — verbose tags |
| Machine parse | ★★★ — indent trap, anchor complexity | ★★★★★ — strict, fast (1 pass) | ★★★★ — well-formed check |
| Comments | Yes # | No (JSON5/JSONC) | Yes <!-- --> |
| Schema/Validation | Via JSON Schema (YAML is JSON superset) | JSON Schema | XSD, DTD — strong |
| Namespaces | No | No | Yes xmlns — collision safe |
| Attributes | No — map to nesting | No | Yes <tag attr="v"> |
| Mixed content | No | No | Yes text <b>bold</b> text |
| Size (wire) | Small (no tags/brackets) | Small (~ same as YAML) | Large (open+close tags) |
| Binary | No — base64 string | No — base64 string | No — base64 string |
| Streaming | Poor — needs whole doc | NDJSON per line OK | SAX streaming OK |
Schema: JSON Schema validates both YAML (convert to JSON first) and JSON — one schema. XML's XSD is stronger for mixed content and namespaces but heavier. Streaming: large JSON array [100K] needs full parse; NDJSON {"id":1}\n{"id":2} per ndjson.org streams; XML SAX streams tag by tag.
When to Use Which — Decision Guide With Real Examples
- YAML — Config human-edited, comments, anchors: Kubernetes manifests
apiVersion: v1\nkind: Pod,compose.ymlservices: web: image: nginx, GitHub Actions.github/workflows/ci.ymljobs: build: runs-on: ubuntu-latest, Ansiblesite.yml. Chosen because engineers edit it daily and need# commentand anchor reusedefaults: &defaults {timeout: 5}→<<: *defaults. Explore via YAML tools. - JSON — Machine-to-machine strict interchange: REST APIs
fetch("/api/user").then(r=>r.json())→{"user":{"id":42}},package.json, tsconfig.json, OpenAPI, GeoJSON, logs{"level":"info","msg":"request"}, browserlocalStorage JSON.stringify. Chosen because every parser is strict, fast, native to JS (JSON.parse), and no indent trap. Explore via JSON tools. - XML — Documents with mixed content, namespaces, legacy:
SOAP<Envelope>, RSS/Atom<item><title>, SVG<svg xmlns="http://www.w3.org/2000/svg"><g>, Officedocx(zipped XML), XHTML<div>text <b>bold</b></div>where text and tags intermix. Mixed contenttext <b>bold</b> moreis impossible in JSON/YAML without hacks. Explore via XML tools.
Real example: compose.yml uses YAML (human writes services: web: with comment # web tier); fetch("/api/compose") returns JSON (machine reads {"services":{"web":{"image":"nginx"}}}); that same config rendered as SVG dashboard uses XML (<svg><g transform="...">). Pipeline: YAML in repo → CI parses YAML → converts to JSON for API → UI renders SVG XML — three formats, one data, choice by audience.
Quick Rule
Human writes config weekly with comments → YAML. Service sends data per second with strict parse → JSON. Document has mixed content/namespaces or legacy SOAP → XML. When in doubt: API → JSON, K8s → YAML, Office/RSS → XML.
Convert Losslessly — YAML ↔ JSON (XML Harder)
YAML ↔ JSON is lossless: YAML is a superset of JSON — every {"a":1} is valid YAML, so converting adds "quotes" and ,: but preserves numbers, booleans, null, nesting. Example:
# YAML ↔ JSON
name: Ada ↔ {"name": "Ada"}
age: 30 ↔ {"age": 30}
tags: ↔ "tags": ["js","yaml"]
- js
- yaml
Paste YAML into our YAML to JSON converter → it emits valid JSON with correct "quotes", preserves 30 not "30", and warns on duplicate keys — no regex.
XML → JSON is lossy / convention-dependent: <name>Ada</name> → {"name":"Ada"} but <user id="42">Ada</user>? Attributes need convention: BadgerFish {"user":{"@id":"42","#text":"Ada"}} vs Parker {"user":"Ada"} dropping attrs. No universal — choose mapping and document it. Conversely JSON → XML needs root tag: {"name":"Ada"} → <root><name>Ada</name></root>.
When Loss Matters
YAML port: "3000" (quoted → string) vs port: 3000 (number) → JSON preserves difference "port":"3000" vs "port":3000. XML <port>3000</port> is always string "3000" → parse to number yourself. Always validate after conversion with JSON Schema or XSD.
Pitfalls — Indent, Quotes, Trailing Commas, and Namespaces
| Mistake | Fix |
|---|---|
| YAML indent 2 spaces off | name: Ada (1 level) vs name: Ada (2 levels) → different object; use editor with 2-space, no tabs; validate with YAML linter |
| YAML tab not space | Tabs forbidden per spec — Tab found error → replace with 2 spaces |
| JSON single quotes / trailing comma | {'a':1,} → {"a":1}; see json.org no single quotes, no trailing , |
| JSON unquoted key | {name:"Ada"} → {"name":"Ada"} — all keys "quoted" |
| JSON comments | Pure JSON has no // — use JSON5 or JSONC for config comments, strip before wire |
| XML missing xmlns | <svg> without xmlns="http://www.w3.org/2000/svg" → parser may not render — include per SVG2 |
Decision shortcut: Config human-edited + comments → YAML (but lint indent). Machine API strict → JSON (but validate). Document mixed content/namespaces → XML (but accept verbosity). When tempted to add comments to JSON API, use YAML for repo and convert to JSON for wire via converter.
History — Why Three Close Cousins Diverged
XML (1998, W3C, XML 1.0): designed for documents and interchange when strict validation via DTD/XSD and namespaces dominated (enterprise, SOAP). Success led to XHTML and Office Open XML (docx is zipped XML).
JSON (2001, Douglas Crockford, RFC 8259 via json.org): JavaScript literal subset — web won because JSON.parse was already the engine, not a library. Every language added json.loads (Python), json.Unmarshal (Go) with same text — dumb text beats smart binary on debuggability.
YAML (2001, 1.2 via YAML 1.2.2): became human config favorite as Kubernetes and GitHub Actions chose it for readability and anchor reuse (&defaults), but paid with indentation sensitivity. YAML's superset property is intentional — JSON inside YAML works: {"a": 1} parses as a: 1.
Result: XML for documents/legacy, JSON for APIs, YAML for human config — not because one is better universally but because each audience optimized differently.
Data Types Deep — What Each Can Carry
| Type | YAML | JSON | XML (text) |
|---|---|---|---|
| String | name: Ada or "Ada" | "Ada" must | <name>Ada</name> |
| Number | age: 30 (int) • 3.14 | 30 • 3.14 | 30 as string, parse yourself |
| Boolean | active: true (true/false, yes/no in YAML 1.1 trap) | true/false lower | true string |
| Null | spouse: null or ~ | null | xsi:nil="true" or empty |
| Date | 2024-01-01 → YAML auto-date! | "2024-01-01T00:00:00Z" string | 2024-01-01 string |
| Binary | !!binary | R0lGOD... | "R0lGOD..." base64 string | R0lGOD... base64 string |
YAML's auto-typing is a trap: yes, 0123, 1.0 in YAML 1.1 may parse as boolean, string, float — use quoted "yes" if you mean string. JSON is predictable — no magic types, always parse yourself. XML has no types — all content is string until XSD says xs:integer. Binary always base64 string in all three — none is binary wire like MessagePack.
Dates — The Classic Trap
YAML 2024-01-01 without quotes may become Date object, JSON "2024-01-01T00:00:00Z" stays string until new Date(), XML <date>2024-01-01</date> stays string. Always quote dates in YAML if you want string.
Performance and Size — Wire Bytes and Parse Speed
On wire, YAML and JSON are similar (≈1.5× vs XML tags): name: Ada (9) vs {"name":"Ada"} (15) vs <name>Ada</name> (17). After gzip, gap shrinks. Parse speed: JSON strict grammar JSON.parse is C-optimized in engines — 2-5× faster than YAML (indentation + anchors) and XML (well-formedness + namespace). For 100K records streaming, JSON NDJSON {"id":1}\n{"id":2} per ndjson.org streams line-by-line; YAML poor streaming (needs whole doc for anchors); XML SAX streams tag-by-tag. Choose NDJSON/JSON Lines for logs, not YAML.
JSON: 45KB minified, YAML: 47KB, XML: 82KB (tags). Gzipped: 9KB, 9KB, 12KB — similar; parse: JSON 12ms, YAML 28ms, XML 35ms.
Measure on your data — narrow tables favor CSV more than any of these three.
Tooling — Linters, Validators, and Migration
Validate early: yamllint for YAML indent, jq empty file.json or python -m json.tool for JSON, xmllint --noout file.xml for XML well-formedness. Editor: VS Code highlights YAML indent errors and JSON trailing commas before commit. CI: fail build if jq empty non-zero. Migration path: repo config in YAML for humans → CI converts to JSON via yq -o json for API that expects JSON — no hand-rewrite.
Advanced YAML — Anchors, Aliases, and Merge That JSON Can't Do
defaults: &defaults
timeout: 5
retries: 3
service_a:
<<: *defaults # merge defaults
name: api
service_b:
<<: *defaults
name: worker
YAML's &defaults anchor + *defaults alias + <<: *defaults merge lets you DRY config — JSON has no such feature (copy-paste). This is why Helm charts and compose files use YAML for reuse without templating. Pitfall: anchors require whole-document parse — breaks streaming and makes diff noisy if you over-anchor. Use for 2-3 repeats, not 20.
YAML Auto-Typing Gotchas
Unquoted yes/no, on/off, 0123 may become boolean or octal in YAML 1.1 — Norway: NO (country) became boolean false. YAML 1.2 fixes but many parsers linger. Quote strings that look like booleans: country: "NO". See YAML spec core schema.
JSON Variants — JSON5 and JSONC for Config Comments
Pure JSON per RFC 8259 forbids // comment and trailing , — {"a":1,} // comment fails JSON.parse. Two supersets allow them for human config:
- JSON5: single quotes, trailing commas, comments
//—{'a':1, // comment }valid → used inbabelrc. See JSON5. - JSONC (JSON with Comments): VS Code's
tsconfig.jsonandsettings.jsonallow//and trailing commas — editor parses, but API must strip before wire. VS Code setting:"jsonc"language.
Rule: store repo config as JSONC/YAML for humans if your tool supports, but send pure JSON over network — strip comments before fetch or API will 400. Formatter can strip to pure.
XML Deep — Namespaces, Attributes, and Mixed Content Power
XML's strength is where JSON/YAML weak:
- Namespaces:
<svg xmlns="http://www.w3.org/2000/svg"><g><circle/></g></svg>prevents tag collision when embedding SVG inside XHTML — both have<title>. JSON/YAML have no namespaces — collision via nesting only. - Attributes vs Elements:
<user id="42" active="true">Ada</user>attributes for metadata, elements for content — convention matters becauseidas attribute vs<id>42</id>changes JSON mapping (@id). - Mixed content:
<p>text <b>bold</b> more <i>italic</i></p>interleaves text and tags — common in docs, impossible in JSON/YAML without array-of-nodes hack[{"type":"text","v":"text "},{"type":"b","v":"bold"}].
XML also has well-formedness (single root, closed tags) and validity via XSD strong typing — namespaces + XSD is why SOAP/wsdl and docx use XML.
XML Pitfall — Closing and Entities
Missing </tag> or unescaped & in AT&T → must be AT&T → parser error xmlParseEntityRef: no name. Always escape < > & " ' in content. Check with xmllint --noout file.xml.
Security — YAML Deserialization vs JSON Safe Parse
YAML's power is risk: Python's yaml.load (not safe_load) executes arbitrary objects — !!python/object/apply:os.system → RCE. See OWASP Deserialization. Use yaml.safe_load or yaml.safeLoad (js-yaml). JSON JSON.parse is safe — no code execution, only 6 types. XML has XXE — <!ENTITY xxe SYSTEM "file:///etc/passwd"> → XXE — disable external entities. Choose JSON for untrusted input — safest parser.
# Python
import yaml, json
yaml.safe_load(text) # not yaml.load
json.loads(text) # safe
# JS
yaml.load(text, {schema: yaml.JSON_SCHEMA}) // safe
JSON.parse(text) // safe
Never yaml.load on user input — use safe.
Performance — Parsing Cost and When It Matters
Human cost dominates machine cost for config (K8s manifest 2KB parses in 1ms in any format), but wire matters for APIs at scale. JSON wins for machine parse: strict grammar, no indentation stack, no anchor table — V8's JSON.parse is C++-optimized and 2-5× faster than YAML (js-yaml) and XML (DOMParser) on same 100K rows. XML's DOM builds tree + namespace resolution; YAML resolves tags and anchors. Example: 10K-row JSON array 2MB parses 15ms JSON vs 38ms YAML vs 42ms XML on Node. If your API serves 10K RPS, JSON's 20ms saving is 200 CPU seconds/second — capacity. For config parsed once at boot, YAML's cost is negligible — choose readability.
When Human Cost Beats Machine Cost
K8s deployment.yaml edited 10×/week — YAML's anchors save copy-paste and comments explain # 3 replicas for HA. API serving 1M requests/day — JSON's speed saves servers. Document with <p>text <b>bold</b> <i>italic</i></p> — XML's mixed content saves hacks. Optimize for who edits most.
Binary Alternatives — When Text Is Too Big
All three are UTF-8 text — readable but verbose vs binary. Alternatives keep same data model but binary wire:
- MessagePack, CBOR, BSON: JSON data model binary — size ~30% smaller, parse faster, keep JSON schema. Use when wire size matters (IoT, game sync) and both sides support — convert via
msgpacklib, not human edit. - Protocol Buffers, Avro, Thrift: schema-first binary — smaller and typed, need schema registry. Use for internal RPC at scale, not browser fetch (needs codegen).
For web browser fetch, JSON remains default because fetch(...).json() is one line and dev tools show pretty text. Only switch to binary internal services after profiling shows JSON gzip still too big.
Migration — Moving Between Formats Without Losing Data
Path: YAML in repo (human) → CI converts to JSON for service that expects JSON via yq -o=json config.yaml > config.json or converter tool — lossless. JSON → XML needs root and attribute convention choice — document @attr for attributes. Always keep one source of truth — repo stores YAML, build emits JSON, not both edited. Validate after each conversion with jq empty (JSON), yamllint, xmllint --noout.
# Repo: config.yaml (human with comments)
# CI: yq eval -o=json config.yaml | jq . > config.json # add quotes, validate
# API: reads config.json via JSON.parse — strict, fast
# Docs: same config rendered as XML for doc tool: json2xml config.json > config.xml
One source, multiple emits — never edit two.
Tooling — Lint, Validate, and Diff
Validate early: yamllint config.yaml for indent, jq empty file.json or python -m json.tool for JSON, xmllint --noout file.xml for well-formedness, xmllint --schema schema.xsd for valid. Editor: VS Code highlights YAML indent errors and JSON trailing commas before commit. Diff: pretty JSON (2-space) diffs show real change; minified diff is noise. Commit pretty, emit minified for wire.
Decision Checklist — Answer These 3, Pick
- Who edits weekly? Human with comments → YAML; machine strict → JSON; doc with mixed content → XML.
- Where does it travel? Browser
fetch→ JSON (one line.json()); K8s API → YAML (kubectl applies YAML); legacy SOAP/RSS → XML viaxmllint. - Need namespaces/attributes? Yes → XML; No → YAML/JSON.
If you answered "human + no namespaces" → YAML. "Machine + strict" → JSON. "Document/legacy" → XML. Hybrid is common: repo YAML → CI JSON for wire → SVG XML for render — one data, three emits, choice by audience as intro pipeline showed.
Real Stacks — Two Teams That Pick Differently
Platform team (K8s): Helm chart values.yaml (YAML with anchors &defaults) → CI yq to JSON for validator webhook → deployed as K8s manifest YAML. UI dashboard same data via JSON API fetch("/api/deploy").json(). Docs rendered as SVG XML.
API team (REST): OpenAPI JSON openapi.json served via {"openapi":"3.0"} — JSON strict, no comments, fast parse at 10K RPS. Config in repo is JSONC (comments) but emitted pure JSON for clients.
Both pass audit: YAML yamllint, JSON jq empty, XML xmllint --noout — each format linted per its spec, no universal linter.
yamllint k8s/*.yaml && jq empty api/*.json && xmllint --noout docs/*.xml && echo ok
git diff --pretty # committed pretty shows which key was added when age changed
Fail CI if any invalid — catch indent vs trailing comma before deploy.
Keep one source — committed pretty YAML/JSON for diffs, wire minified for size — formatter toggles both in one paste.
Validate at build — jq empty && yamllint && xmllint --noout fails fast before deploy, not at 3am when JSON.parse on bad file crashes request.
Copy working YAML name: Ada vs JSON {"name":"Ada"} template — one correct reused beats four hand-typed with different quote or indent mistakes.
Version your config like code — git diff on pretty shows which key was added when age changed, not reformat noise.
Validate early, fail fast at boot — not at 3am when JSON.parse on bad file crashes request.
Keep one source — committed pretty for diffs, wire minified for size — choose per context.
Validate at build — jq empty fails fast before deploy.
Frequently Asked Questions
What is the difference between YAML, JSON, and XML?
YAML: human config, indentation, # comment, superset of JSON — no braces. JSON: strict {"key":"val"} with "quotes" mandatory, no comments — native to JS, web APIs. XML: <tag>val</tag> with attributes/namespaces — verbose, document markup. Same data can be all three.
Is YAML a superset of JSON?
Yes per YAML 1.2 — every valid JSON is valid YAML (e.g., {"a":1} parses as YAML). Not vice versa: YAML name: Ada # comment is not JSON. So YAML → JSON converts losslessly; JSON → YAML is also fine.
When should I use YAML vs JSON?
YAML for human-edited config (K8s, compose, CI) needing comments and anchors; JSON for machine interchange (REST, package.json) needing strict parse and speed. Store in repo as YAML, send over wire as JSON via converter if needed.
When should I use XML over JSON?
XML for documents with mixed content (text <b>bold</b> more), namespaces (xmlns), or legacy SOAP/RSS/docx. JSON can't model mixed content without hacks. For new APIs, JSON is default.
Can I convert YAML to JSON and back without loss?
YAML ↔ JSON is lossless for data (types preserved, quotes added/removed) except YAML comments/anchors are lost in JSON (JSON has no comments). XML → JSON is lossy for attributes/namespaces — choose convention (@attr) and document.
Which is faster to parse?
JSON — strict grammar, one pass, native JSON.parse optimized in engines. YAML needs full parser (anchors, tags). XML needs well-formedness + namespace resolution. For 100K records, JSON parse is typically 2-5× faster than YAML/XML.