In the era of Large Language Models and token-conscious applications, every character counts. Today I’m excited to introduce zio-toon, a Scala 3 library that brings TOON (Token-Oriented Object Notation) to the ZIO ecosystem with a focus on performance, type safety, and functional programming best practices.
TOON is a line-oriented, indentation-based text format that dramatically reduces token usage compared to JSON — typically by 30-60%. This makes it perfect for LLM interactions where context windows and token costs are critical constraints.
But zio-toon isn’t just about token efficiency. It’s built from the ground up with ZIO’s principles: effect-oriented programming, composable dependency injection, type-safe error handling, and streaming support.
Add to your build.sbt:
libraryDependencies += "io.github.riccardomerolla" %% "zio-toon" % "0.1.8"
Let’s start with TOON’s killer feature: tabular arrays. Consider this common scenario — you’re building a user management API that needs to efficiently transmit user data to an LLM for analysis:
import io.github.riccardomerolla.ziotoon._
import ToonValue._
import zio._
case class User(id: Int, name: String, role: String, department: String)
val users = List(
User(1, "Alice Johnson", "Engineering Manager", "Backend"),
User(2, "Bob Smith", "Senior Developer", "Frontend"),
User(3, "Charlie Brown", "DevOps Engineer", "Infrastructure"),
User(4, "Diana Prince", "Product Manager", "Product"),
User(5, "Eve Chen", "Data Scientist", "Analytics")
)
// Convert to TOON format
val usersToon = arr(
users.map(u => obj(
"id" -> num(u.id),
"name" -> str(u.name),
"role" -> str(u.role),
"department" -> str(u.department)
)): _*
)
val program = for {
encoded <- Toon.encode(obj("users" -> usersToon))
_ <- Console.printLine(encoded)
} yield ()
This produces incredibly compact output:
users[5]{id,name,role,department}:
1,Alice Johnson,Engineering Manager,Backend
2,Bob Smith,Senior Developer,Frontend
3,Charlie Brown,DevOps Engineer,Infrastructure
4,Diana Prince,Product Manager,Product
5,Eve Chen,Data Scientist,Analytics
Compare this to the equivalent JSON (573 characters vs 284 characters — a 50% reduction!):
{"users":[{"id":1,"name":"Alice Johnson","role":"Engineering Manager","department":"Backend"},{"id":2,"name":"Bob Smith","role":"Senior Developer","department":"Frontend"},{"id":3,"name":"Charlie Brown","role":"DevOps Engineer","department":"Infrastructure"},{"id":4,"name":"Diana Prince","role":"Product Manager","department":"Product"},{"id":5,"name":"Eve Chen","role":"Data Scientist","Analytics"}]}
One of zio-toon’s standout features is seamless integration with zio-schema for type-safe encoding and decoding:
import zio.schema._
case class APIResponse(
success: Boolean,
message: String,
data: List[User],
metadata: ResponseMetadata
)
case class ResponseMetadata(
timestamp: Long,
version: String,
requestId: String
)
object APIResponse {
given schema: Schema[APIResponse] = DeriveSchema.gen[APIResponse]
}
object ResponseMetadata {
given schema: Schema[ResponseMetadata] = DeriveSchema.gen[ResponseMetadata]
}
object User {
given schema: Schema[User] = DeriveSchema.gen[User]
}
// Real-world API response encoding
val apiResponse = APIResponse(
success = true,
message = "Users retrieved successfully",
data = users,
metadata = ResponseMetadata(
timestamp = System.currentTimeMillis(),
version = "1.0.0",
requestId = "req-12345"
)
)
val schemaProgram = for {
// Type-safe encoding - compile-time guarantees
encoded <- ToonSchemaService.encode(apiResponse)
_ <- Console.printLine("Encoded response:")
_ <- Console.printLine(encoded)
// Type-safe decoding - returns APIResponse, not generic JSON
decoded <- ToonSchemaService.decode[APIResponse](encoded)
_ <- Console.printLine(s"Decoded ${decoded.data.length} users")
} yield decoded
// Provide the necessary layers
val layers = ToonJsonService.live >>> ToonSchemaService.live
schemaProgram.provide(layers)
When dealing with large datasets — imagine processing millions of log entries or financial transactions — memory efficiency becomes crucial. zio-toon’s streaming support lets you process data without loading everything into memory:
import zio.stream._
case class LogEntry(
timestamp: Long,
level: String,
service: String,
message: String,
userId: Option[String]
)
// Simulate a large log file processing scenario
val logStream: ZStream[Any, Nothing, LogEntry] = ZStream.fromIterable(
(1 to 1000000).map { i =>
LogEntry(
timestamp = System.currentTimeMillis() + i,
level = if (i % 100 == 0) "ERROR" else if (i % 10 == 0) "WARN" else "INFO",
service = s"service-${i % 5}",
message = s"Processing request $i",
userId = if (i % 3 == 0) Some(s"user-${i % 1000}") else None
)
}
)
// Convert LogEntry to ToonValue
def logEntryToToon(entry: LogEntry): ToonValue = obj(
"timestamp" -> num(entry.timestamp),
"level" -> str(entry.level),
"service" -> str(entry.service),
"message" -> str(entry.message),
"userId" -> entry.userId.map(str).getOrElse(ToonValue.Null)
)
val streamingProgram = for {
// Process logs in batches, memory-efficient
_ <- Console.printLine("Processing large log dataset...")
processed <- ToonStreamService.encodeStream(
logStream.map(logEntryToToon)
).take(100) // Take first 100 for demo
.runCollect
_ <- Console.printLine(s"Processed ${processed.length} log entries")
// Stream back to objects for analysis
decoded <- ToonStreamService.decodeStream(
ZStream.fromIterable(processed)
).runCollect
errorCount = decoded.count {
case obj if obj.fields.exists(_._2.toString.contains("ERROR")) => true
case _ => false
}
_ <- Console.printLine(s"Found $errorCount error entries")
} yield ()
val streamLayers = (ToonEncoderService.live ++ ToonDecoderService.live) >+> ToonStreamService.live
streamingProgram.provide(streamLayers)
Real-world applications need robust error handling. zio-toon provides built-in retry policies for various failure scenarios:
import io.github.riccardomerolla.ziotoon.ToonRetryPolicy._
import io.github.riccardomerolla.ziotoon.ToonRetryOps._
// Simulate an unreliable data source
def unreliableDataSource: ZIO[Any, Throwable, String] =
Random.nextBoolean.flatMap { reliable =>
if (reliable) ZIO.succeed("""
metrics[3]{name,value,unit}:
cpu_usage,85.2,percent
memory_usage,2048,mb
disk_io,150,ops_per_sec
""")
else ZIO.fail(new RuntimeException("Network timeout"))
}
val resilientProgram = for {
_ <- Console.printLine("Fetching metrics with retry policy...")
// Smart retry: only retries recoverable errors
rawData <- unreliableDataSource
.retry(smartRetry)
.timeoutFail(new RuntimeException("Total timeout"))(30.seconds)
// Decode with additional retry for parse errors
metrics <- ToonDecoderService.decode(rawData)
.retryDefault // Built-in exponential backoff
.catchAll {
case ToonError.ParseError(msg) =>
Console.printLine(s"Parse error: $msg") *>
ZIO.succeed(ToonValue.Null)
case error =>
Console.printLine(s"Unrecoverable error: ${error.message}") *>
ZIO.fail(error)
}
_ <- Console.printLine("Metrics retrieved successfully!")
} yield metrics
resilientProgram.provide(ToonDecoderService.live)
For applications migrating from JSON or needing interoperability, zio-toon provides seamless conversion with detailed analytics:
case class PerformanceReport(
service: String,
metrics: Map[String, Double],
alerts: List[String],
recommendations: List[String]
)
val jsonReport = """
{
"service": "user-api",
"metrics": {
"response_time": 245.5,
"error_rate": 0.02,
"throughput": 1250.8,
"cpu_usage": 78.2
},
"alerts": ["High CPU usage detected", "Response time above threshold"],
"recommendations": ["Scale up instances", "Optimize database queries", "Enable caching"]
}
"""
val migrationProgram = for {
_ <- Console.printLine("Converting JSON to TOON...")
// Parse JSON to ToonValue
toonValue <- ToonJsonService.fromJson(jsonReport)
// Encode to TOON format
toonString <- ToonEncoderService.encode(toonValue)
_ <- Console.printLine("TOON format:")
_ <- Console.printLine(toonString)
// Calculate token savings
savings <- ToonJsonService.calculateSavings(toonValue)
_ <- Console.printLine(f"\n📊 Token Analysis:")
_ <- Console.printLine(f"JSON size: ${savings.jsonSize} characters")
_ <- Console.printLine(f"TOON size: ${savings.toonSize} characters")
_ <- Console.printLine(f"Savings: ${savings.savingsPercent}%.1f%% (${savings.savings} characters)")
// Verify round-trip integrity
backToJson <- ToonJsonService.toJson(toonValue)
_ <- Console.printLine("\nâś… Round-trip conversion successful")
} yield savings
val jsonLayers = ToonEncoderService.live ++ ToonDecoderService.live ++ ToonJsonService.live
migrationProgram.provide(jsonLayers)
zio-toon showcases ZIO’s power through several key patterns:
All operations are pure descriptions that compose beautifully:
val pipeline = for {
raw <- fetchData
parsed <- ToonDecoderService.decode(raw)
validated <- validateBusinessRules(parsed)
enriched <- enrichWithMetadata(validated)
saved <- persistToDatabase(enriched)
} yield saved
Clean separation of concerns with testable components:
// Custom encoder with business logic
class BusinessToonEncoder(
validator: ValidationService,
logger: LoggingService
) extends ToonEncoderService {
def encode(value: ToonValue): UIO[String] =
for {
_ <- logger.info("Encoding business object")
validated <- validator.validate(value)
encoded <- ToonEncoderService.encode(validated)
_ <- logger.debug(s"Encoded ${encoded.length} characters")
} yield encoded
}
val businessLayer = ZLayer.fromFunction(BusinessToonEncoder.apply)
Type-safe error management without exceptions:
sealed trait BusinessError extends Throwable
case class ValidationError(field: String, reason: String) extends BusinessError
case class EncodingError(underlying: ToonError) extends BusinessError
def processUserData(userData: String): ZIO[ToonDecoderService, BusinessError, ProcessedUser] =
for {
parsed <- ToonDecoderService.decode(userData)
.mapError(EncodingError.apply)
validated <- validateUser(parsed)
.mapError(ValidationError.apply)
processed <- processBusinessLogic(validated)
} yield processed
Based on the included JMH benchmarks, zio-toon delivers significant advantages:
zio-toon is designed for gradual adoption. You can start with the pure API for simple use cases and gradually migrate to the full ZIO effect system as your needs grow:
// Simple start - pure API
val result = Toon.encode(myData)
// Growing complexity - effect API
val program = Toon.encode(myData).provide(Toon.live)
// Full power - streaming, retry policies, custom layers
val production = complexPipeline
.retry(smartRetry)
.provide(fullProductionLayers)
zio-toon represents a new approach to serialization in the Scala ecosystem — one that prioritizes efficiency, type safety, and functional programming principles. Whether you’re building LLM-integrated applications, high-performance APIs, or data processing pipelines, zio-toon provides the tools you need.
The library follows the official TOON Format Specification v2.0 and includes comprehensive test coverage, JMH performance benchmarks, and real-world examples.
Check out the project:
sbt "benchmarks/Jmh/run" to see performance metricszio-toon is production-ready with 93+ unit tests, property-based testing, and integration tests. It demonstrates ZIO best practices while solving real problems in the era of token-conscious computing.
Built with ❤️ for the Scala and ZIO communities. Questions? Open an issue or discussion on GitHub!
zio-toon: Where functional programming meets efficient serialization. 🚀