YAML Codec Module
zio-blocks-schema-yaml is a schema-driven YAML codec module for serializing and deserializing Scala types to and from YAML format. It provides comprehensive encoding and decoding with support for 27 primitive types, records, variants, sequences, maps, and recursive types. Core types: Yaml, YamlCodec, YamlCodecDeriver, YamlOptions.
The module integrates with a pure-Scala YAML parser and writer to provide human-readable serialization with optional JSON interoperability, configuration options for pretty-printing, and automatic schema generation.
Motivation​
YAML is a human-readable data format that appears widely across configuration files, Kubernetes manifests, CI/CD pipelines, and data serialization. Manually writing YAML encoders and decoders is error-prone and repetitive, especially for complex types with records, nested structures, and recursive definitions. zio-blocks-schema-yaml eliminates this friction by deriving codec instances directly from your Scala types using ZIO Schema. You describe your data shape once, and the module handles:
- Full YAML type support (mappings, sequences, scalars, null)
- Automatic schema generation from Scala types
- Pretty-printed and compact formatting options
- JSON interoperability for seamless conversion
- Precise error reporting with location traces showing the path to errors
- Recursive type support with automatic cycle detection
- Multiple encoding paths: byte arrays and strings
- Multiple decoding paths: byte arrays and strings
- Cross-platform compatibility (JVM and Scala.js)
Rather than hand-writing YAML parsing logic or relying on external libraries with limited Scala support, you work with strongly-typed schemas that the compiler validates.
Installation​
Add the module to your build.sbt:
libraryDependencies += "dev.zio" %% "zio-blocks-schema-yaml" % "0.0.51"
For Scala.js, use %%% instead of %%:
libraryDependencies += "dev.zio" %%% "zio-blocks-schema-yaml" % "0.0.51"
Supported Scala versions: 2.13.x and 3.x
Introduction​
The module provides a complete pipeline for YAML codec derivation and usage:
- Define your type — Any Scala type with a
Schemainstance - Derive a codec — Use
Schema[A].derive(YamlFormat)to obtain aYamlCodec[A] - Encode or decode — Call
codec.encodeToString(value)orcodec.decode(yamlString) - Handle errors — Catch
SchemaErrorwith location traces showing where the error occurred - Configure output — Use
YamlOptionsfor pretty-printing or compact formatting
The derivation process is automatic for all supported types (all 27 primitives, records, variants, sequences, maps). The module automatically generates YAML-compatible formats and handles encoding/decoding without manual configuration.
How They Work Together​
The YAML codec pipeline flows through these layers:
1. User defines Schema[A] for their type
↓
2. Schema[A].derive(YamlFormat) creates YamlCodec[A]
↓
3. YamlCodecDeriver derives Encoder and Decoder implementations
- For primitives: type-specific YAML scalar encoders/decoders
- For records: field-by-field composition with YAML mapping
- For variants: union encoding with discriminators
- For sequences: YAML sequence encoding/decoding
- For maps: YAML mapping encoding/decoding
↓
4. YamlCodec provides multiple encoding paths
- encodeToString(value) → String
- encode(value) → Array[Byte]
↓
5. YamlCodec provides multiple decoding paths
- decode(yaml: String) → Either[SchemaError, A]
- decode(bytes: Array[Byte]) → Either[SchemaError, A]
↓
6. YamlOptions controls output formatting
- Pretty-printing with document markers
- Compact output without markers
- Custom indentation and style
↓
7. Yaml AST provides structured representation
- Mapping (key-value pairs)
- Sequence (ordered lists)
- Scalar (string values with optional tags)
- NullValue (YAML null)
↓
8. Errors include location traces
Shows path (.field[index].nested) to error location
Typical workflow:
A user type flows through the derivation and encoding pipeline as follows:
User type (e.g., case class Config)
↓
Schema.derived (automatic via macro)
↓
Schema[Config].derive(YamlFormat) → YamlCodec[Config]
↓
Use codec.encodeToString(config) to serialize → String
Use codec.decode(yamlString) to deserialize → Either[SchemaError, Config]
↓
Handle SchemaError with location trace on failure
Type Relationships​
Yaml— Sealed trait AST representing YAML data structuresYamlCodec[A]— Main public API; contains encoder and decoder for bidirectional serializationYamlCodecDeriver— Configuration and derivation system; generates codecs from SchemaYamlOptions— Configuration for output formatting (pretty, compact, indentation)YamlReader,YamlWriter— Low-level YAML parsing and serialization
Common Patterns​
This section shows practical patterns for working with YAML codecs in real-world scenarios.
Pattern 1: Derive and Encode a Configuration Record​
To derive and use a YAML codec for a record type:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
case class AppConfig(name: String, port: Int, debug: Boolean)
object AppConfig {
implicit val schema: Schema[AppConfig] = Schema.derived[AppConfig]
}
val codec = AppConfig.schema.derive(YamlFormat)
val config = AppConfig("MyApp", 8080, true)
val yamlString = codec.encodeToString(config)
println(yamlString)
// name: MyApp
// port: 8080
// debug: true
Pattern 2: Decode YAML with Error Handling​
When decoding YAML data, errors include location traces showing where the problem occurred.
To decode YAML and handle errors with location information:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
case class DatabaseConfig(host: String, port: Int, username: String)
object DatabaseConfig {
implicit val schema: Schema[DatabaseConfig] = Schema.derived
}
val codec = DatabaseConfig.schema.derive(YamlFormat)
val yaml = """
host: localhost
port: invalid
username: admin
"""
val result = codec.decode(yaml)
result match {
case Right(config) => println(s"Loaded: $config")
case Left(error) =>
println(s"Error: ${error.getMessage}")
}
Pattern 3: Pretty-Print with Configuration​
Use YamlOptions to control output formatting with indentation and document markers.
To encode with pretty-printing:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
case class Server(name: String, endpoints: List[String], timeout: Int)
object Server {
implicit val schema: Schema[Server] = Schema.derived
}
val codec = Server.schema.derive(YamlFormat)
val server = Server("api-server", List("GET /health", "POST /data"), 30)
val prettyYaml = codec.encodeToString(server)
// ---
// name: api-server
// endpoints:
// - GET /health
// - POST /data
// timeout: 30
Pattern 4: Handle Recursive Types​
Recursive types (types that reference themselves) are fully supported with automatic cycle detection.
To define and encode a recursive data structure:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
sealed trait TreeNode
case class Leaf(value: String) extends TreeNode
case class Branch(label: String, children: List[TreeNode]) extends TreeNode
object TreeNode {
implicit val schema: Schema[TreeNode] = Schema.derived
}
val codec = TreeNode.schema.derive(YamlFormat)
val tree: TreeNode = Branch("root", List(Leaf("a"), Branch("b", List(Leaf("c")))))
val yaml = codec.encodeToString(tree)
Pattern 5: YAML-JSON Interoperability​
Convert between YAML and JSON representations seamlessly using YamlJsonInterop.
To convert Yaml to Json:
import zio.blocks.schema.yaml._
import zio.blocks.schema.json._
val yamlString = """
name: Alice
age: 30
"""
val yaml = YamlReader.read(yamlString)
val json = yaml.toJson
// json represents the same data as JSON
Yaml​
Sealed trait representing a YAML node. Provides a complete AST for YAML data with four possible cases: Mapping, Sequence, Scalar, and NullValue.
Overview​
Yaml is a sealed trait that represents any valid YAML value. It can be constructed directly or derived from codecs.
YAML AST Structure​
To work with the YAML AST directly:
import zio.blocks.schema.yaml._
// Create a mapping (key-value pairs)
val mapping = Yaml.Mapping.fromStringKeys(
"name" -> Yaml.Scalar("Alice"),
"age" -> Yaml.Scalar("30")
)
// Create a sequence (ordered list)
val sequence = Yaml.Sequence(
Yaml.Scalar("item1"),
Yaml.Scalar("item2")
)
// Create a scalar (string value)
val scalar = Yaml.Scalar("hello")
// The null value
val nullValue = Yaml.NullValue
Printing YAML​
Format YAML nodes using compact or pretty-printed output:
import zio.blocks.schema.yaml._
val yaml = Yaml.Mapping.fromStringKeys(
"name" -> Yaml.Scalar("Bob"),
"active" -> Yaml.Scalar("true")
)
val compact = yaml.print
// name: Bob
// active: true
val pretty = yaml.printPretty
// ---
// name: Bob
// active: true
Converting to JSON​
Transform Yaml nodes to JSON for interoperability:
import zio.blocks.schema.yaml._
val yaml = Yaml.Scalar("42")
val json = yaml.toJson
YamlCodec[A]​
Main codec type for encoding and decoding values to and from YAML. Contains encoder and decoder for bidirectional serialization.
Overview​
YamlCodec[A] holds both an encoder and decoder, providing a complete solution for serializing and deserializing values in YAML format. The codec is derived automatically from a Schema[A].
Encoding Values to String​
Use the codec to convert values to YAML strings:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
case class Person(name: String, email: String)
object Person {
implicit val schema: Schema[Person] = Schema.derived
}
val codec = Person.schema.derive(YamlFormat)
val person = Person("Alice", "alice@example.com")
val yaml = codec.encodeToString(person)
Encoding Values to Byte Array​
Convert values to YAML bytes:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
case class Data(timestamp: Long, value: String)
object Data {
implicit val schema: Schema[Data] = Schema.derived
}
val codec = Data.schema.derive(YamlFormat)
val data = Data(System.currentTimeMillis(), "sample")
val bytes = codec.encode(data)
Decoding Values from String​
Use the codec to convert YAML strings back to values:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
case class Config(host: String, port: Int)
object Config {
implicit val schema: Schema[Config] = Schema.derived
}
val codec = Config.schema.derive(YamlFormat)
val yaml = """
host: localhost
port: 8080
"""
val result: Either[zio.blocks.schema.SchemaError, Config] = codec.decode(yaml)
Decoding Values from Byte Array​
Read and decode values from YAML bytes:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
case class Settings(debug: Boolean, timeout: Int)
object Settings {
implicit val schema: Schema[Settings] = Schema.derived
}
val codec = Settings.schema.derive(YamlFormat)
// Use encoded bytes from a previous encoding
val settings = Settings(debug = true, timeout = 30)
val bytes = codec.encodeToString(settings).getBytes("UTF-8")
val result = codec.decode(bytes)
YamlCodecDeriver​
Configuration and derivation system for creating YamlCodec[A] instances from Schema[A].
Overview​
YamlCodecDeriver implements the schema-driven derivation of YAML codecs. It automatically handles 27 primitive types and complex types (records, variants, sequences, maps), generating appropriate YAML encoders and decoders.
How Derivation Works​
To create a codec from a schema:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
case class User(id: Int, name: String, active: Boolean)
object User {
implicit val schema: Schema[User] = Schema.derived
}
val codec = User.schema.derive(YamlFormat)
Primitive Type Support​
All 27 ZIO Schema primitives are supported:
- Numeric:
Byte,Short,Int,Long,Float,Double,BigInt,BigDecimal - Logical:
Boolean,Char,String - Temporal:
Instant,LocalDate,LocalDateTime,LocalTime,Duration,Period,Year,YearMonth,MonthDay,Month,DayOfWeek,ZonedDateTime,OffsetDateTime,OffsetTime,ZoneId,ZoneOffset - Special:
UUID,Currency,Unit
Record Type Support​
Case classes (records) are fully supported. Each field becomes a named key in the YAML mapping:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
case class Address(street: String, city: String, zipCode: String)
object Address {
implicit val schema: Schema[Address] = Schema.derived
}
val codec = Address.schema.derive(YamlFormat)
Variant Type Support​
Sealed traits and sum types are encoded as YAML mappings with discriminator information:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
sealed trait Status
case class Running(pid: Int) extends Status
case class Stopped(exitCode: Int) extends Status
object Status {
implicit val schema: Schema[Status] = Schema.derived
}
val codec = Status.schema.derive(YamlFormat)
YamlOptions​
Configuration for YAML output formatting. Controls indentation, document markers, and presentation style.
Overview​
YamlOptions provides predefined configurations and factory methods for customizing YAML serialization output.
Pretty-Printing Configuration​
Enable pretty-printed output with document markers:
import zio.blocks.schema.yaml._
val prettyOptions = YamlOptions.pretty
// Enables:
// - Document markers (---)
// - Indentation (default 2 spaces)
// - Readable formatting
Compact Output​
Use compact formatting without document markers:
import zio.blocks.schema.yaml._
val compactOptions = YamlOptions.default
// Disables document markers for inline or file formats
Error Handling​
YAML decoding errors include location traces showing the path through nested structures where the error occurred.
Understanding Error Traces​
Errors render as paths like .field[0].nested.value showing exactly where decoding failed:
import zio.blocks.schema._
import zio.blocks.schema.yaml._
case class Team(name: String, members: List[String])
object Team {
implicit val schema: Schema[Team] = Schema.derived
}
val codec = Team.schema.derive(YamlFormat)
val invalidYaml = """
name: Engineering
members: invalid
"""
val result = codec.decode(invalidYaml)
result match {
case Right(team) => println(s"Loaded: $team")
case Left(error) =>
println(s"Error: ${error.getMessage}")
}
Zero-Overhead Error Handling​
Errors use zero-overhead exceptions (no stack traces) for efficient error reporting in scenarios where errors are expected and handled inline.