If you’ve sent JSON to an LLM API and gotten a bill or a context-limit error that seemed way too big for the data you actually sent, this is why. LLMs don’t read text the way we do. They break it into chunks called tokens first, and JSON happens to be one of the more token-expensive ways to represent the same information. Let’s get into what that actually means and what you can do about it — with real token counts, not estimates.

What a Token Actually Is

A token isn’t a word, and it isn’t a character either. It’s somewhere in between, a chunk of text that a language model’s tokenizer decided to treat as one unit. Common whole words often become a single token. Longer or less common words get split into a few pieces. Punctuation, quotes, and brackets frequently become their own tokens too.

That last part matters a lot for JSON specifically. Every {, every ", every :, every , is potential token overhead that plain prose or a lighter format just wouldn’t have.

Why This Matters for LLM APIs

Two reasons, and they’re both practical, not academic.

You’re billed per token, in and out, for most LLM APIs. A JSON payload that’s more token-heavy than it needs to be costs you real money at scale, even if the actual information inside it is small.

Context windows are measured in tokens, not characters or “amount of data.” Send a large, verbose JSON blob and you can burn through a meaningful chunk of your available context before the model’s even gotten to your actual question.

How We Measured This: A Real Tokenizer, Not a Character Count

We ran a real tokenizer on real payloads: tiktoken with the cl100k_base encoding — the tokenizer family used by GPT-4-class models — and cross-checked every number with the newer o200k_base encoding. The two encodings agree within about 1.5% on every format below, so these results aren’t an artifact of one particular tokenizer.

The test payload is 50 records, built from a fixed recipe so anyone can reproduce it: id running 1–50, name cycling through Alex, Sam, Jordan, Taylor, Casey, Morgan, Riley, and Jamie, role alternating admin / editor, and active always true.

One honest caveat: the earlier version of this post measured character counts and showed only two example records, so its exact original 50-record payload isn’t recoverable. The recipe above reproduces those earlier character counts within 20 characters on every format (pretty 4,215 vs 4,235; minified 2,714 vs 2,734; flattened 1,363 vs 1,383), so the token numbers below are representative of the same test, measured on a payload you can rebuild exactly.

The Test: Same Data, Three Structures

We took the same 50 records, structured three different ways, and counted real tokens for each. The first two records looked like this (the test used 50, all the same shape):

Pretty-printed, one object per record with full key names repeated every time, standard 2-space indentation:

[
  {
    "id": 1,
    "name": "Alex",
    "role": "admin",
    "active": true
  },
  {
    "id": 2,
    "name": "Sam",
    "role": "editor",
    "active": true
  }
]

Minified, same structure, whitespace stripped:

[{"id":1,"name":"Alex","role":"admin","active":true},{"id":2,"name":"Sam","role":"editor","active":true}]

Flattened, key names stated once in a header, then just the values:

{"columns":["id","name","role","active"],"rows":[[1,"Alex","admin",true],[2,"Sam","editor",true]]}

Here’s what came out of it, at 50 records (tiktoken, cl100k_base):

FormatCharactersTokensReduction vs. pretty (tokens)
Pretty-printed4,2151,570—
Minified2,71482047.8%
Flattened (header + rows)1,36343372.4%

Minifying alone gets you a solid win, which lines up with what we found testing minification for other reasons in our minify JSON guide. But look at the flattened version. Cutting the repeated key names, which is the part almost nobody thinks about, got us all the way to a 72% token reduction against the original — and the flattened payload is 47.2% smaller in tokens than even the minified version. That gap only grows as the row count grows, since you’re paying for “id”, “name”, “role”, and “active” as literal text, over and over, once per record, in the standard array-of-objects shape.

Does it hold at 500 records?

Yes. We re-ran the same recipe at 500 records, same three structures:

FormatCharactersTokensReduction vs. pretty (tokens)
Pretty-printed42,64315,688—
Minified27,6428,18847.8%
Flattened (header + rows)13,6914,20173.2%

The percentages barely move. Minifying saves the same 47.8% at 500 records as at 50, and flattening saves slightly more (73.2% vs 72.4%), because the one-time header cost matters even less at scale. This isn’t a small-payload quirk.

What the Numbers Actually Say

Three things in the results are worth spelling out, because the old character-count version of this post got two of them wrong.

Minifying saves more tokens than characters. At 50 records, minifying cuts characters by 35.6% but tokens by 47.8%. Character counts understated the win by a wide margin, because the indentation and line breaks in pretty-printed JSON cost more tokens per character than the data itself does. If you only measure characters, minifying looks like a modest optimisation. In tokens — the thing you’re actually billed for — it’s nearly half the cost gone.

The “~4 characters per token” rule doesn’t hold for JSON. Pretty-printed JSON in this test runs at 2.68 characters per token (4,215 characters / 1,570 tokens). Minified JSON is 3.31 characters per token, flattened 3.15. The 4-characters rule of thumb comes from English prose, where common words compress into single tokens. JSON is mostly punctuation, quotes, and repeated short keys — exactly the text that tokenizers split into small pieces. Use the rule of thumb for prose; measure real tokens for JSON.

Per record, the difference is stark. Pretty-printed, each record costs about 31.4 tokens (1,570 / 50). Flattened, each record costs about 8.7 tokens (433 / 50). At 1,000 requests a day, that’s 1,137 tokens saved per request — over 1.1 million tokens a day — from restructuring alone, before you change a single field.

Why This Happens

Arrays of objects are the most natural way to represent a list of records in JSON, and also the most wasteful when you’re optimizing for an LLM specifically. Every single record repeats every single key name in full. Fifty records means the word “active” shows up fifty separate times as plain text, contributing nothing new each time beyond the actual true/false value sitting next to it.

A columnar or “header plus rows” structure states each key exactly once, then just lists the values. Genuinely less readable to a human glancing at it, genuinely lighter for whatever’s tokenizing it.

Practical Ways to Cut Token Usage

  • Minify before sending. No reason to pay token cost for indentation and line breaks the model doesn’t need to understand the data. In our test this alone cut tokens by 47.8%.
  • Flatten repeated-key arrays into a columns-plus-rows structure when you’re sending a lot of records and don’t specifically need each one to be a self-contained object. In our test this cut tokens by 72.4% against pretty-printed JSON.
  • Drop fields the model doesn’t actually need for the task at hand. Sending your entire database record when the model only needs three fields out of fifteen is pure waste.
  • Avoid unnecessary nesting. Every extra layer of { } is extra punctuation, and extra punctuation is extra tokens.
  • Shorten key names where you control the schema, though weigh this against your own code’s readability. "active" versus "a" is a real token difference at scale, but it costs you something too if a human has to read that JSON later.

If you’re already reformatting JSON before sending it somewhere, our JSON Formatter and Validator handles the minify step directly in your browser, which covers the easiest win on this list with zero extra tooling.

Frequently Asked Questions

Is there an exact character-to-token ratio I can rely on?

Not a precise one. It varies by language, by how common the specific words and symbols are, and by which model’s tokenizer you’re using. And for JSON specifically, the familiar “~4 characters per token” rule of thumb is wrong: we measured 2.68 characters per token for pretty-printed JSON, 3.31 for minified, and 3.15 for flattened. Treat any ratio as a rough guide, and count real tokens when the number matters.

Should I always flatten arrays of objects before sending them to an LLM?

Not always. It’s a real win when you’re sending a lot of uniform records and token cost matters — 72.4% fewer tokens at 50 records in our test, 73.2% at 500. For smaller payloads, or when you need the model to reason about individual records as self-contained objects, the standard array-of-objects shape is often worth the extra tokens for the clarity it gives the model.

Does minifying JSON change what the model understands from it?

No. Whitespace carries no meaning in JSON, so removing it doesn’t change the data at all, just the token cost of transmitting it. In our test, minifying cut tokens by 47.8% with zero change to the data.

Are there tools that count tokens exactly for a specific model?

Yes, model providers typically publish or link to their own tokenizer libraries or web-based counters for exactly this purpose. The numbers in this post were produced with tiktoken (cl100k_base, cross-checked against o200k_base), which is OpenAI’s open-source tokenizer library. Worth using one directly if you need a precise number for a specific model rather than the general result covered here.

Does the saving hold up at larger scale, or is it a small-payload effect?

It holds up. We re-ran the identical test at 500 records: minifying still saved 47.8% of tokens, and flattening saved 73.2% — slightly more than at 50 records, because the one-time header cost matters even less as the row count grows.

Summary

JSON’s structure, specifically the habit of repeating full key names across every object in an array, makes it more token-expensive than it needs to be when you’re sending it to an LLM. We measured it with a real tokenizer this time — tiktoken, cl100k_base — instead of character counts, and the real numbers are stronger than the old estimates: minifying cuts tokens by 47.8% (not the 35.5% the character counts suggested), and flattening repeated keys cuts tokens by 72.4% against pretty-printed JSON, rising to 73.2% at 500 records. Minify first, flatten when you’re sending a lot of uniform records, and only send the fields the model actually needs.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *