Amaresh PelletiYAML vs JSON for LLMs, measured with a real tokenizer: YAML cuts ~30% of tokens vs JSON, and one YAML parsing quirk already breaks GitHub Actions configs.
Originally published on DevToolHub.
YAML vs JSON for LLMs isn't just a style preference — it changes how many tokens you pay for on every request. I ran both formats through a real tokenizer on identical data instead of repeating the "20-ish percent" figures floating around online, and the gap is bigger than you'd guess, with one real parsing bug that can silently corrupt an LLM's output if you're not careful.
Most "YAML saves tokens" claims cite a number without showing their work. So I built two test cases — a nested config object and a 10-row uniform table — and ran both formats through OpenAI's cl100k_base tokenizer (the GPT-4-era encoding, a standard reference point; exact counts shift slightly on Claude's or Gemini's own tokenizers, but the ranking between formats doesn't).
Nested config (not a list — a single object with child objects):
| Format | Tokens | Characters |
|---|---|---|
| JSON | 99 | 322 |
| YAML | 66 | 223 |
YAML cut 33.3% of the tokens off the same config, just by dropping braces, commas, and quoted keys.
Uniform table (10 rows, 4 fields each — the shape most AI agent data actually takes: user lists, logs, tool outputs):
| Format | Tokens | Characters |
|---|---|---|
| JSON | 323 | 963 |
| YAML | 221 | 590 |
| TOON | 97 | 235 |
Here the picture changes. YAML still beats JSON by 31.6%, but TOON — a format built specifically for this case — beats JSON by 70% and beats YAML by 56.1%.
The reason is structural, not cosmetic. YAML's - id: 1\n name: Alice syntax repeats every field name on every row. TOON declares the field list once (users[10]{id,name,role,active}:) and streams bare values after it. For a one-off config file that repetition barely matters. For a 500-row tool result going into an agent's context window on every turn, it adds up fast.
A practical pattern: keep your actual system in JSON, and only convert to YAML or TOON for the text you hand to the model — then have the model hand JSON back for anything that needs to go back into code.
YAML's compactness comes from a looser grammar than JSON's, and that looseness has a well-documented failure mode: the Norway problem. In YAML 1.1 (still what many libraries implement, including older PyYAML), bare words like no, yes, on, and off parse as booleans, not strings. A country-code list like:
geoblock_regions:
- dk
- fi
- is
- no
- se
silently becomes ["dk", "fi", "is", false, "se"] — Norway (no) turns into False. Version-looking strings have the same problem: 10.23 parses as the float 10.23, not the string "10.23", which breaks version-comparison logic that expected text.
This isn't theoretical for this site's audience specifically. GitHub Actions workflow files use on: as a top-level key (on: push). Per a real bug report against the Go YAML library, a generic YAML 1.1 parser reads that key as the boolean true, not the string "on" — so any tooling that parses workflow YAML with a non-GitHub-aware library can silently drop or mishandle the trigger config. GitHub's own runner handles on: correctly because it's YAML-1.2-aware, but if you're writing your own workflow linter, generator, or an LLM-based tool that reads .github/workflows/*.yml, this is exactly the kind of bug that passes code review and fails in production. See our GitHub Actions YAML guide for the rest of the syntax.
The fix, per that same thread, is one of two things: quote the key ("on": push) or use a YAML 1.2-compliant parser. YAML 1.2.2 — the current spec, revised 2021-10-01 — dropped the bare on/off/yes/no boolean shortcuts specifically because of bugs like this one. If you're feeding YAML into an LLM and expect it to reliably reason about or regenerate that YAML, this gotcha matters doubly: the model has almost certainly also been trained on YAML 1.1-style parsing assumptions.
A single chat message where YAML saves 60 tokens over JSON is immaterial. It compounds somewhere specific: AI agents that re-send accumulated context — conversation history, tool outputs, retrieved documents — on every turn. If an agent calls a tool 20 times in a session and each tool result is a JSON array of rows, you're paying the JSON tax 20 times over, not once. That's the same economics covered in our prompt compression guide: the lever that matters is the one you pull on every request, not the one you pull once.
Q: Is YAML actually faster for an LLM to parse, or just fewer tokens?
A: This article only measured token count, which is the cost and context-window lever. Parsing speed is a separate question and depends on the runtime, not the model.
Q: Does TOON replace YAML?
A: No — they overlap only on uniform tabular data. For nested, irregular structures (most real config files), TOON's own spec uses YAML-style indentation, so there's no efficiency gain from switching.
Q: Why does "no" become false in YAML?
A: YAML 1.1 treats a set of bare words (y, n, yes, no, on, off, true, false) as boolean literals when unquoted. YAML 1.2.2 removed this behavior from the spec, but many parsers (older PyYAML, Go's go-yaml v2) still implement 1.1 rules for compatibility.
Q: Should I quote every YAML value to avoid this?
A: Quote any value that could be misread as a different type — country codes, version strings, and anything resembling yes/no/on/off. For a config key like GitHub Actions' on:, quote the key itself: "on": push.
Quick Summary:
no → false) is real and has broken actual tooling, including GitHub Actions' on: key in non-GitHub-aware parsersIf you're weighing YAML vs JSON for LLMs in your own pipeline, the honest answer is "it depends on the shape of the data," not "YAML always wins" — measure your own payloads with a tokenizer before committing, and watch for unquoted values that look like booleans.