JSON is comfortable. Familiar. Everywhere.
But it’s also wasteful. Every time we send a list of objects, we pay the cost of repeating the same field names again and again "id" a hundred times, "name" a hundred times silently taxing bandwidth and slowing real-world performance.
Meanwhile, the LLM world quietly optimized around this problem.
Token-oriented formats like TOON, which treat data like a spreadsheet (headers once, values below), already power faster inference and cheaper processing at massive scale. They get real efficiency gains simply by avoiding redundancy.
That raised the obvious question:
If AI companies already benefit from TOON, why aren’t our apps?
So for the Kiroween Hackathon’s Skeleton Crew challenge, I set out to build a compact, flexible serialization format that brings those same advantages to everyday Kotlin developers without forcing anyone to rewrite their models or architecture. Using Kiro’s spec-driven workflow to design before coding, I built the foundation for KToon, a TOON-style format that plugs directly into kotlinx.serialization and works with your existing @Serializable classes.
One clean skeleton. Many possible applications.
Next, I’ll walk through what makes TOON useful in practice and how KToon turns a normal data class into a dramatically smaller payload without changing a single line inside it.
What TOON Really Is (and Why It Works)
Think about a spreadsheet.
You define your columns once id, name, age and then you just fill in the rows. No one would ever rewrite the column names on every line; that would be absurd. Yet that’s exactly what JSON does every time we serialize a list of structured objects.
TOON flips that model on its head. It treats structured data collections as tables instead of repeated key-value objects. The structure lives once at the top. Everything below is just values.
A simple example speaks louder than theory:
JSON
[
{"id": 1, "name": "Alice", "age": 30},
{"id": 2, "name": "Bob", "age": 25}
]
TOON
users[2]{id,name,age}:
1,Alice,30
2,Bob,25
Same meaning. Same structure.
But dramatically fewer characters especially as datasets grow.
The results scale fast: when you have thousands of rows, the headers stay exactly the same size, while JSON grows linearly with duplicated keys. TOON cuts the redundancy out of the serialisation pipeline entirely.
And when data is hierarchical, not tabular, TOON switches into a clear, indentation-based representation instead of forcing everything into a grid. It’s human-readable without being verbose, and structured without being rigid.
Why this matters
- Faster network responses (less data, fewer tokens)
- Lower costs in bandwidth-sensitive environments
- Smaller offline storage footprints
- Faster parsing, especially on constrained devices A structure that aligns with how humans already think about lists This idea isn’t abstract the LLM ecosystem already runs on token-efficient formats like this because it saves real money at scale. We’re simply borrowing a proven trick and applying it where mobile and multiplatform apps need it just as much.
How KToon Fits into kotlinx.serialization
The best part about building KToon wasn’t inventing a new format.
It was realizing we didn’t need to reinvent the entire serialization ecosystem to support it.
Kotlin already gives us a powerful abstraction layer with kotlinx.serialization.
At the core of that system are two extensibility points:
an Encoder that defines how values are written, and
a Decoder that defines how values are read back
Everything else reflection, annotations, polymorphism, nested objects, nullability is handled by the framework. The serialization engine walks through your object graph and calls into whatever format implementation you provide.
Meaning:
If you implement your own encoder/decoder, you automatically get compatibility with every @Serializable data class without modifying any of them.
That’s the breakthrough that made KToon possible.
Rather than invent custom DSLs, code generators, or special DTO structures, KToon simply acts as a StringFormat implementation. The serialization machinery already knows how to traverse a list of User, or a ProductCatalog, or a tree of nested objects. KToon just decides how to write them.
It’s the software equivalent of replacing the wheels without redesigning the car.
You don’t rewrite @Serializable data class User(...) you tell serialization to express it differently.
That’s what makes KToon a skeleton worth building on:
thin, intentional, and flexible enough to power completely different applications without accumulating framework weight.
Building the Engine, Encoder, Decoder, and Lexer
The heart of KToon lives in three small but tightly synchronized components. Each exists for one purpose, nothing more. No grand abstractions, no “just in case” layers. Like bones in a good skeleton, they do less so the whole system can do more.
The Encoder, Structure Without Buffering
ToonEncoder extends Kotlin’s AbstractEncoder, and that’s where the magic really begins. As the serialization framework walks through the object graph, calling methods like encodeString(), encodeInt(), or beginStructure(), we decide what to write and when. The encoder runs a small state machine with four modes:
IDLE — waiting for the next field
ENCODING_STRUCTURE — writing nested objects using indentation
ENCODING_COLLECTION — preparing to output a table
ENCODING_VALUE — writing primitive values
Encounter a list? Instead of holding the entire collection in memory, the encoder immediately delegates to ToonCollectionEncoder, which writes a header like:
users[5]{id,name,email,age}:
Then each element hands control to ToonRowEncoder, which writes CSV values line by line:
1,Alice,alice@example.com,28
The delegations are deliberate:
the main encoder handles structure,
the collection encoder handles headers,
the row encoder handles values.
No one knows more than it needs to. No buffering. No temporary model. Everything streams straight into a StringBuilder in one pass O(N) time and minimal memory footprint.
The Decoder, Context-Aware Parsing and Real Error Messages
Decoding is harder than encoding; anyone who has built a parser knows the pain. You’re not just reading you’re validating structure, honoring indentation, counting rows, matching types, and explaining mistakes gracefully.
ToonDecoder uses a ToonLexer to tokenize input line by line, tracking indentation level and mapping fields back to properties. When it hits table mode, it verifies the declared size matches reality and parses each row with ToonRowDecoder.
Its real superpower: human-readable error messages.
Example:
Type mismatch at line 4, field 'age': cannot decode 'invalid' as Int
Context:
2: users[5]{id,name,email,age}:
3: 1,Alice,alice@example.com,28
>>> 4: 2,Bob,bob@example.com,invalid
5: 3,Charlie,charlie@example.com,42
Instead of stack traces, you get answers.
The Lexer Tokenizing With Indentation Awareness
ToonLexer sits between raw text and structured understanding it splits lines, tracks indentation, and identifies patterns like headers, CSV values, and delimiters. Because TOON is line-oriented, indentation gives structure and table headers give shape.
Separating lexing from decoding keeps both components clean, testable, and predictable.
Plugging Into Ktor One Line to Swap Formats
Once the engine existed, Ktor integration felt almost embarrassingly simple. Ktor already exposes an extension point for serialization formats via ContentConverter. We wrap the TOON engine and expose one toon() extension:
install(ContentNegotiation) {
json() // Keep JSON
toon() // Add TOON
}
Request Content-Type: application/toonget TOON.
Request JSON get JSON.
Same route code. Zero switching cost (on client side).
Real-World Impact, 67.4% Fewer Tokens
In our sample endpoint, JSON produced 553 bytes (~138 tokens).
TOON produced 179 bytes (~45 tokens).
A 67.4% reduction.
And the bigger the dataset, the bigger the gap.
This isn’t theoretical efficiency:
for inference pipelines sending product catalogs, search results, or event logs to LLMs, this means faster context windows and lower token bills.
For mobile apps, it means snappier UX and lighter data usage.
How Kiro Shaped the Development Process
This skeleton didn’t emerge from hacking in the dark. It was shaped intentionally using Kiro’s spec-driven workflow.
Before writing code, every major component encoder, decoder, lexer, Ktor integration lived first as words:
requirements, architecture sketches, constraints, trade-offs.
When decisions got messy, like how to handle nulls in tables or whether indentation levels should equal structure depth, MCP agents acted like design partners challenging assumptions, connecting dots, forcing clarity. They didn’t generate code. They generated understanding.
That discipline is what kept KToon around 1000 lines of pure Kotlin instead of spiraling into a maze of abstractions.
If KToon had been built “vibe-first”, it would have collapsed under its own cleverness.
Built spec-first, it became a skeleton instead of spaghetti.
Why This Skeleton Matters
KToon doesn’t try to replace JSON everywhere.
It exists to solve a very real problem: redundant field names in structured collections.
The foundation is intentionally small:
pure Kotlin, no platform code,
single-pass O(N) performance,
works with any existing @Serializable classes,
one-line integration with Ktor,
and a clean surface for future extensions (streaming, binary mode, Retrofit, etc.).
Most importantly, it proves something worth remembering:
See Project here: JosephSanjaya/ktoon
The techniques that make AI faster and cheaper don’t need to stay inside LLM labs.
They can make our everyday apps better too.

Top comments (0)