Skip to main content
Version: 2.x

XML

Xml is a sealed trait representing XML nodes. It provides a type-safe, immutable representation of all valid XML document structures including elements, text nodes, CDATA sections, comments, and processing instructions.

sealed trait Xml {
def xmlType: XmlType
def is(xmlType: XmlType): Boolean
def as(xmlType: XmlType): Option[xmlType.Type]
def unwrap(xmlType: XmlType): Option[xmlType.Unwrap]
def print: String
def printPretty: String
def select: XmlSelection
}

Xml supports:

  • Type-safe representation of all XML node types
  • Fluent navigation and querying with XmlSelection
  • Schema-derived automatic codec generation
  • Zero external dependencies
  • Full cross-platform support (JVM and Scala.js)

Overview​

Zero-dependency XML codec for ZIO Blocks Schema with cross-platform support.

The schema-xml module provides automatic XML codec derivation for any type with a Schema. It includes a complete XML AST, fluent navigation API, and support for XML-specific features like attributes and namespaces.

Key features:

  • Zero Dependencies: No external XML libraries required
  • Cross-Platform: Full support for JVM and Scala.js
  • Schema-Based: Automatic codec derivation from Schema definitions
  • XML AST: Complete representation of XML documents
  • Fluent API: Navigation and transformation with XmlSelection
  • Attributes: First-class support for XML attributes via annotations
  • Namespaces: XML namespace support with prefix handling

Installation​

To use the schema-xml module, add the following dependency to your build.sbt:

libraryDependencies += "dev.zio" %% "zio-blocks-schema-xml" % "0.0.14"

Basic Usage​

Start by deriving an XML codec from your Schema definition:

Deriving Codecs​

To create an XML codec, use Schema[A].derive(XmlFormat):

import zio.blocks.schema._
import zio.blocks.schema.xml._

case class Person(name: String, age: Int)

object Person {
implicit val schema: Schema[Person] = Schema.derived
}

// Derive XML codec using the unified format API
val codec = Schema[Person].derive(XmlFormat)

Encoding to XML​

Encode your values to XML using the codec:

import zio.blocks.schema._
import zio.blocks.schema.xml._

case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
}

val codec = Schema[Person].derive(XmlFormat)
val person = Person("Alice", 30)

Encode to XML bytes:

val bytes: Array[Byte] = codec.encode(person)
// bytes: Array[Byte] = Array(
// 60,
// 80,
// 101,
// 114,
// 115,
// 111,
// 110,
// 62,
// 60,
// 110,
// 97,
// 109,
// 101,
// 62,
// 65,
// 108,
// 105,
// 99,
// 101,
// 60,
// 47,
// 110,
// 97,
// 109,
// 101,
// 62,
// 60,
// 97,
// 103,
// 101,
// 62,
// 51,
// 48,
// 60,
// 47,
// 97,
// 103,
// 101,
// 62,
// 60,
// 47,
// 80,
// 101,
// 114,
// 115,
// 111,
// 110,
// 62
// )

Encode to XML string:

val xmlString: String = codec.encodeToString(person)
// xmlString: String = "<Person><name>Alice</name><age>30</age></Person>"

Encode to pretty-printed XML:

val prettyXml = codec.encodeToString(person, WriterConfig.pretty)
// prettyXml: String = """<Person>
// <name>Alice</name>
// <age>30</age>
// </Person>"""

Decoding from XML​

Decode XML strings or bytes back to your typed values:

import zio.blocks.schema._
import zio.blocks.schema.xml._

case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
}

val codec = Schema[Person].derive(XmlFormat)

// Decode from XML string
val xml = "<Person><name>Alice</name><age>30</age></Person>"

Decode the XML string and see the result:

val result: Either[SchemaError, Person] = codec.decode(xml)
// result: Either[SchemaError, Person] = Right(
// Person(name = "Alice", age = 30)
// )

You can also decode from bytes:

val bytes = xml.getBytes("UTF-8")
val fromBytes: Either[SchemaError, Person] = codec.decode(bytes)
// fromBytes: Either[SchemaError, Person] = Right(
// Person(name = "Alice", age = 30)
// )

XML AST​

The Xml ADT represents all valid XML node types:

Xml
├── Xml.Element (element with name, attributes, children)
├── Xml.Text (character data)
├── Xml.CData (unparsed character data)
├── Xml.Comment (XML comment)
└── Xml.ProcessingInstruction (processing instruction)

Creating XML Nodes​

Construct XML nodes directly using the case class constructors:

import zio.blocks.schema.xml._
import zio.blocks.chunk.Chunk

// Create elements
val simple = Xml.Element("person")
val withChildren = Xml.Element("person", Xml.Text("Alice"))

// Create with XmlName (for namespaces)
val namespaced = Xml.Element(
XmlName("person", "http://example.com"),
Chunk.empty,
Chunk.empty
)

// Create text nodes
val text = Xml.Text("Hello, World!")
val cdata = Xml.CData("<script>...</script>")

// Create comments and processing instructions
val comment = Xml.Comment("This is a comment")
val pi = Xml.ProcessingInstruction("xml-stylesheet", "href=\"style.css\"")

XmlName​

XmlName represents an element or attribute name with optional namespace. Create instances with different namespace configurations:

import zio.blocks.schema.xml.XmlName

// Local name only
val simple = XmlName("person")
simple.localName // "person"
simple.namespace // ""
simple.prefix // ""

// With namespace URI
val ns = XmlName("person", "http://example.com/ns")

// With prefix (for prefixed elements like atom:feed)
val prefixed = XmlName("feed", Some("atom"), None)
prefixed.localName // "feed"
prefixed.prefix.contains("atom") // true

XmlBuilder​

Construct XML documents programmatically with a fluent API:

import zio.blocks.schema.xml._

// Build an element with attributes and children
val doc = XmlBuilder.element("person")
.attr("id", "123")
.attr("status", "active")
.child(XmlBuilder.element("name").text("Alice").build)
.child(XmlBuilder.element("age").text("30").build)
.build

// Result:
// <person id="123" status="active">
// <name>Alice</name>
// <age>30</age>
// </person>

// Create other node types
val textNode = XmlBuilder.text("content")
val cdataNode = XmlBuilder.cdata("<![CDATA[raw content]]>")
val commentNode = XmlBuilder.comment("comment text")

Configuration​

The schema-xml module provides configuration options for both parsing and writing:

WriterConfig​

Use WriterConfig to control XML output formatting:

import zio.blocks.schema.xml.WriterConfig

// Compact output (default)
val compact = WriterConfig.default
// <Person><name>Alice</name></Person>

// Pretty-printed with 2-space indentation
val pretty = WriterConfig.pretty
// <Person>
// <name>Alice</name>
// </Person>

// With XML declaration
val withDecl = WriterConfig.withDeclaration
// <?xml version="1.0" encoding="UTF-8"?>
// <Person><name>Alice</name></Person>

// Custom configuration
val custom = WriterConfig(
indentStep = 4,
includeDeclaration = true,
encoding = "UTF-8"
)
OptionDefaultDescription
indentStep0Spaces per indentation level (0 = compact)
includeDeclarationfalseInclude XML declaration
encoding"UTF-8"Character encoding in declaration

ReaderConfig​

Controls XML parsing behavior and security limits:

import zio.blocks.schema.xml.ReaderConfig

// Default configuration
val default = ReaderConfig.default

// Custom limits
val custom = ReaderConfig(
maxDepth = 100,
maxAttributes = 50,
maxTextLength = 1000000,
preserveWhitespace = true
)
OptionDefaultDescription
maxDepth1000Maximum element nesting depth
maxAttributes1000Maximum attributes per element
maxTextLength10000000Maximum text content length
preserveWhitespacefalsePreserve whitespace in text nodes

Attributes​

Encode case class fields as XML attributes using the @xmlAttribute annotation:

import zio.blocks.schema._
import zio.blocks.schema.xml._

case class Person(
@xmlAttribute() id: String,
@xmlAttribute("status") active: String,
name: String,
age: Int
)

object Person {
implicit val schema: Schema[Person] = Schema.derived
}

val codec = Schema[Person].derive(XmlFormat)
val person = Person("123", "active", "Alice", 30)

Encode the person with attributes:

val xml = codec.encodeToString(person)
// xml: String = "<Person><id>123</id><active>active</active><name>Alice</name><age>30</age></Person>"

The @xmlAttribute annotation accepts an optional custom name:

  • @xmlAttribute() - Uses the field name as the attribute name
  • @xmlAttribute("customName") - Uses the provided name as the attribute name

Namespaces​

Support for XML namespaces with the @xmlNamespace annotation:

import zio.blocks.schema._
import zio.blocks.schema.xml._

@xmlNamespace(uri = "http://www.w3.org/2005/Atom", prefix = "atom")
case class Feed(
title: String,
updated: String
)

object Feed {
implicit val schema: Schema[Feed] = Schema.derived
}

val codec = Schema[Feed].derive(XmlFormat)
val feed = Feed("My Blog", "2024-01-01T00:00:00Z")

Encode the feed with a namespace prefix:

val xml = codec.encodeToString(feed)
// xml: String = "<Feed><title>My Blog</title><updated>2024-01-01T00:00:00Z</updated></Feed>"

Without a prefix (default namespace):

@xmlNamespace(uri = "http://www.w3.org/2005/Atom")
case class Feed(title: String)

object Feed {
implicit val schema: Schema[Feed] = Schema.derived
}

val codec = Schema[Feed].derive(XmlFormat)
val feed = Feed("My Blog")

Encode the feed with default namespace:

val xml = codec.encodeToString(feed)
// xml: String = "<Feed><title>My Blog</title></Feed>"

XmlSelection​

XmlSelection provides a fluent API for navigating and querying XML structures:

Navigate to child elements, filter by type, and extract content:

import zio.blocks.schema.xml._

val xml = XmlReader.read("""
<library>
<books>
<book id="1">
<title>Functional Programming</title>
<author>Alice</author>
</book>
<book id="2">
<title>Advanced Scala</title>
<author>Bob</author>
</book>
</books>
</library>
""").toOption.get

// Navigate to child elements
val books = xml.select.get("library").get("books")

// Navigate by index
val firstBook = books.get("book")(0)

Extract text content from the first book:

val title: Either[XmlError, String] = firstBook.get("title").text
// title: Either[XmlError, String] = Left(
// zio.blocks.schema.xml.XmlError: Expected single value but got 0
// )

You can also navigate descendants recursively:

val allTitles = xml.select.descendant("title")
// allTitles: XmlSelection = XmlSelection(
// Right(
// IndexedSeq(
// Element(
// name = XmlName(localName = "title", prefix = None, namespace = None),
// attributes = IndexedSeq(),
// children = IndexedSeq(Text("Functional Programming"))
// ),
// Element(
// name = XmlName(localName = "title", prefix = None, namespace = None),
// attributes = IndexedSeq(),
// children = IndexedSeq(Text("Advanced Scala"))
// )
// )
// )
// )

Filtering​

Filter selections by node type or custom predicates:

import zio.blocks.schema.xml._

val selection: XmlSelection = ???

// Filter by type
val elements = selection.elements
val texts = selection.texts
val comments = selection.comments

// Custom filtering
val filtered = selection.filter(xml => xml.is(XmlType.Element))

Terminal Operations​

Execute a selection to extract values or convert to other formats:

import zio.blocks.schema.xml._

val selection: XmlSelection = ???

// Get single value (fails if not exactly one)
val one: Either[XmlError, Xml] = selection.one

// Get any value (first of many)
val any: Either[XmlError, Xml] = selection.any

// Get all values as a single XML element
val all: Either[XmlError, Xml] = selection.all

// Convert to chunk
val chunk = selection.toChunk

// Extract text content
val text: Either[XmlError, String] = selection.text
val allText: String = selection.textContent

Combinators​

Combine and transform selections using monadic operations:

import zio.blocks.schema.xml._

val selection1: XmlSelection = ???
val selection2: XmlSelection = ???

// Map over selections
val mapped = selection1.map(xml => xml)

// FlatMap for chaining
val nested = selection1.flatMap(xml => XmlSelection.succeed(xml))

// Combine selections
val combined = selection1 ++ selection2

// Alternative on failure
val withFallback = selection1.orElse(selection2)

XmlPatch​

XmlPatch provides composable XML modification operations:

Creating Patches​

Create patches for add, remove, replace, and attribute operations:

import zio.blocks.schema._
import zio.blocks.schema.xml._

val path = p".library.books.book"

// Add content
val addPatch = XmlPatch.add(
path,
XmlBuilder.element("book").attr("id", "3").build,
XmlPatch.Position.AppendChild
)

// Remove element
val removePatch = XmlPatch.remove(path)

// Replace element
val replacePatch = XmlPatch.replace(
path,
XmlBuilder.element("book").attr("id", "999").build
)

// Set attribute
val attrPatch = XmlPatch.setAttribute(path, "featured", "true")

// Remove attribute
val removeAttrPatch = XmlPatch.removeAttribute(path, "id")

Position Options​

Position options control where new content is inserted relative to the target:

import zio.blocks.schema.xml.XmlPatch.Position

Position.Before // Insert before the target element
Position.After // Insert after the target element
Position.PrependChild // Insert as first child of target
Position.AppendChild // Insert as last child of target

Applying Patches​

Apply a patch to an XML document to produce a modified result:

import zio.blocks.schema._
import zio.blocks.schema.xml._

val xml: Xml = ???
val patch = XmlPatch.setAttribute(p".person", "active", "true")

// Apply the patch
val result: Either[XmlError, Xml] = patch(xml)

Composing Patches​

Combine multiple patches to apply transformations in sequence:

import zio.blocks.schema._
import zio.blocks.schema.xml._

val patch1 = XmlPatch.setAttribute(p".person", "id", "123")
val patch2 = XmlPatch.add(
p".person",
XmlBuilder.element("email").text("alice@example.com").build,
XmlPatch.Position.AppendChild
)

// Compose patches - applies patch1, then patch2
val combined = patch1 ++ patch2

XmlEncoder and XmlDecoder​

For more fine-grained control over XML serialization, use the separate XmlEncoder and XmlDecoder traits:

XmlEncoder​

XmlEncoder[A] provides type-safe XML encoding:

import zio.blocks.schema._
import zio.blocks.schema.xml._

// Automatic derivation from Schema
case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
implicit val encoder: XmlEncoder[Person] = XmlEncoder.fromSchema
}

val person = Person("Alice", 30)
val xml: Xml = XmlEncoder[Person].encode(person)

Creating custom encoders​

Create custom encoders from functions or using contravariance:

import zio.blocks.schema.xml._

// Create from a function
val customEncoder: XmlEncoder[Int] = XmlEncoder.instance(n =>
Xml.Element("number", Xml.Text(n.toString))
)

// Map with contravariance - encode a wrapper type
case class UserId(value: Int)

val userIdEncoder: XmlEncoder[UserId] =
customEncoder.contramap[UserId](_.value)

Using implicit resolution​

Leverage implicit resolution for automatic encoder derivation:

import zio.blocks.schema._
import zio.blocks.schema.xml._

case class Product(id: String, price: Double)
object Product {
implicit val schema: Schema[Product] = Schema.derived
}

// No explicit encoder needed - derives automatically
def encodeProduct[A](value: A)(implicit encoder: XmlEncoder[A]): Xml =
encoder.encode(value)

val result = encodeProduct(Product("item-1", 99.99))

XmlDecoder​

XmlDecoder[A] provides type-safe XML decoding with error handling:

import zio.blocks.schema._
import zio.blocks.schema.xml._

// Automatic derivation from Schema
case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
implicit val decoder: XmlDecoder[Person] = XmlDecoder.fromSchema
}

val xml = Xml.Element("Person",
Xml.Element("name", Xml.Text("Alice")),
Xml.Element("age", Xml.Text("30"))
)

Decode the XML:

val result: Either[XmlError, Person] = XmlDecoder[Person].decode(xml)
// result: Either[XmlError, Person] = Right(Person(name = "Alice", age = 30))

Creating custom decoders​

Create custom decoders from functions or using covariance:

import zio.blocks.schema.xml._
import zio.blocks.chunk.Chunk

// Create from a function
val numberDecoder: XmlDecoder[Int] = XmlDecoder.instance { xml =>
xml match {
case Xml.Element(_, _, Chunk(Xml.Text(text), _*)) =>
text.toIntOption.toRight(XmlError("Invalid number"))
case _ => Left(XmlError("Expected number element"))
}
}

// Map for covariance - decode to a wrapper type
case class UserId(value: Int)

val userIdDecoder: XmlDecoder[UserId] =
numberDecoder.map(UserId(_))

Error handling with decoders​

Handle decoding errors gracefully with fallback strategies:

import zio.blocks.schema._
import zio.blocks.schema.xml._

case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
}

def decodeWithFallback[A](
xml: Xml,
fallback: A
)(implicit decoder: XmlDecoder[A]): A = {
decoder.decode(xml).getOrElse(fallback)
}

val invalidXml = Xml.Element("Empty")
val defaultPerson = Person("Unknown", 0)
val result = decodeWithFallback(invalidXml, defaultPerson)
// Person("Unknown", 0)

Combining encoders and decoders​

Round-trip values by encoding and decoding:

import zio.blocks.schema._
import zio.blocks.schema.xml._
import zio.blocks.schema.xml.syntax._

case class Message(id: String, text: String)
object Message {
implicit val schema: Schema[Message] = Schema.derived
}

// Round-trip: encode then decode
val message = Message("msg-1", "Hello")
val encoded: Xml = message.toXml

Decode the encoded value:

val result: Either[XmlError, Message] =
implicitly[XmlDecoder[Message]].decode(encoded)
// result: Either[XmlError, Message] = Right(
// Message(id = "msg-1", text = "Hello")
// )

Extension Syntax​

When a Schema is in scope, use convenient extension methods:

import zio.blocks.schema._
import zio.blocks.schema.xml._
import zio.blocks.schema.xml.syntax._

case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
}

val person = Person("Alice", 30)

// Encode to XML AST
val xml: Xml = person.toXml

// Encode to XML string
val xmlString: String = person.toXmlString
// <Person><name>Alice</name><age>30</age></Person>

// Encode to bytes
val bytes: Array[Byte] = person.toXmlBytes

// Decode from XML string
val parsed: Either[SchemaError, Person] =
"<Person><name>Bob</name><age>25</age></Person>".fromXml[Person]

// Decode from bytes
val fromBytes: Either[SchemaError, Person] = bytes.fromXml[Person]

Printing XML​

Format XML documents using compact or pretty-printed output. You can convert XML to string with different formatting options:

import zio.blocks.schema.xml._

val xml = Xml.Element("person", Xml.Element("name", Xml.Text("Alice")))

Compact output:

val compact: String = xml.print
// compact: String = "<person><name>Alice</name></person>"

Pretty-printed output:

val pretty: String = xml.printPretty
// pretty: String = """<person>
// <name>Alice</name>
// </person>"""

Custom configuration:

val custom: String = xml.print(WriterConfig(indentStep = 4))

Type Testing and Access​

Test and extract values from XML nodes using type guards and unwrapping:

import zio.blocks.schema.xml._

val xml: Xml = Xml.Element("person")

// Type testing
xml.is(XmlType.Element) // true
xml.is(XmlType.Text) // false

// Type narrowing (returns Option)
val elem: Option[Xml.Element] = xml.as(XmlType.Element)

// Value extraction (returns Option)
val (name, attrs, children) = xml.unwrap(XmlType.Element).get

Supported Types​

All standard ZIO Blocks Schema types are supported:

Numeric Types:

  • Boolean, Byte, Short, Int, Long, Float, Double, Char
  • BigInt, BigDecimal

Text Types:

  • String

Special Types:

  • Unit, UUID, Currency

Java Time Types:

  • Instant, LocalDate, LocalTime, LocalDateTime
  • OffsetTime, OffsetDateTime, ZonedDateTime
  • Duration, Period
  • Year, YearMonth, MonthDay
  • DayOfWeek, Month
  • ZoneId, ZoneOffset

Composite Types:

  • Records (case classes)
  • Variants (sealed traits)
  • Sequences (List, Vector, Set, etc.)
  • Maps (Map[K, V])
  • Options (Option[A])
  • Wrappers (newtypes)

XmlBinaryCodec​

XmlBinaryCodec[A] is the low-level codec interface that bridges Schema definitions with XML serialization. While usually derived automatically, you can work with it directly:

import zio.blocks.schema._
import zio.blocks.schema.xml._

case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
}

// Get the underlying binary codec
val codec: XmlBinaryCodec[Person] =
Schema[Person].derive(XmlBinaryCodecDeriver)

// Encode to Xml directly
val person = Person("Alice", 30)
val xml: Xml = codec.encodeValue(person)

// Decode from Xml directly
val decoded: Either[XmlError, Person] = codec.decodeValue(xml)

XmlBinaryCodec supports all Schema types:

  • Primitives (Int, String, Boolean, etc.)
  • Java time types (Instant, LocalDate, Duration, etc.)
  • Records (case classes with field-level configuration)
  • Variants (sealed traits with discriminators)
  • Collections (List, Vector, Map, etc.)
  • Optional fields (Option[A])
  • Custom wrappers and dynamic values

Error Handling​

All decoding operations return Either[SchemaError, A] or Either[XmlError, A]. The XmlError type provides detailed error information:

import zio.blocks.schema._
import zio.blocks.schema.xml._

case class Person(name: String, age: Int)
object Person {
implicit val schema: Schema[Person] = Schema.derived
}

val codec = Schema[Person].derive(XmlFormat)

// Decoding invalid XML
val invalid = "<Person><name>Alice</name></Person>" // missing age
val result = codec.decode(invalid)

result match {
case Right(person) => println(s"Decoded: $person")
case Left(error) =>
println(s"Error: ${error.getMessage}")
// Error information includes parse location and context
}

XmlError provides detailed error information for debugging:

import zio.blocks.schema.xml._

val error = XmlError("Parse failed")

// Error message
val message: String = error.getMessage

// Get error message for inspection
val errorMsg: String = error.getMessage

Error handling best practices:

import zio.blocks.schema._
import zio.blocks.schema.xml._

case class Config(database: String, port: Int)
object Config {
implicit val schema: Schema[Config] = Schema.derived
}

def loadConfig(xml: String): Either[String, Config] = {
val codec = Schema[Config].derive(XmlFormat)
codec.decode(xml).left.map { error =>
s"Configuration error: ${error.getMessage}\nCheck XML format and required fields."
}
}

Cross-Platform Support​

The XML module works across all platforms:

  • JVM - Full functionality
  • Scala.js - Browser and Node.js

All features including parsing, writing, navigation, and patching work identically on both platforms.

Examples​

These examples demonstrate common use cases and patterns with the XML module:

Complete Example with Attributes and Namespaces​

Define a schema with attributes and namespaces, then encode and decode:

import zio.blocks.schema._
import zio.blocks.schema.xml._

@xmlNamespace(uri = "http://www.w3.org/2005/Atom", prefix = "atom")
case class Entry(
@xmlAttribute() id: String,
title: String,
updated: String,
author: Author
)

case class Author(name: String, email: String)

object Entry {
implicit val authorSchema: Schema[Author] = Schema.derived
implicit val schema: Schema[Entry] = Schema.derived
}

val codec = Schema[Entry].derive(XmlFormat)

val entry = Entry(
id = "entry-1",
title = "First Post",
updated = "2024-01-01T00:00:00Z",
author = Author("Alice", "alice@example.com")
)

Encode the entry with pretty printing:

val xmlStr = codec.encodeToString(entry, WriterConfig.pretty)
// xmlStr: String = """<Entry>
// <id>entry-1</id>
// <title>First Post</title>
// <updated>2024-01-01T00:00:00Z</updated>
// <author>
// <name>Alice</name>
// <email>alice@example.com</email>
// </author>
// </Entry>"""

Decode the XML back to a typed value:

val decoded = codec.decode(xmlStr)
// decoded: Either[SchemaError, Entry] = Right(
// Entry(
// id = "entry-1",
// title = "First Post",
// updated = "2024-01-01T00:00:00Z",
// author = Author(name = "Alice", email = "alice@example.com")
// )
// )

Find elements, extract data, and apply patches to modify XML:

import zio.blocks.schema._
import zio.blocks.schema.xml._

val xmlString = """
<library>
<books>
<book id="1">
<title>Functional Programming</title>
<price>49.99</price>
</book>
<book id="2">
<title>Advanced Scala</title>
<price>59.99</price>
</book>
</books>
</library>
"""

val xml = XmlReader.read(xmlString).toOption.get

// Find all books
val books = xml.select.get("library").get("books").get("book")

// Extract all titles
val titles = books.get("title").toChunk.map { titleElem =>
titleElem.asInstanceOf[Xml.Element].children.head match {
case Xml.Text(text) => text
case _ => ""
}
}
// Chunk("Functional Programming", "Advanced Scala")

// Apply a patch to add a new book
val patch = XmlPatch.add(
p".library.books",
XmlBuilder.element("book")
.attr("id", "3")
.child(XmlBuilder.element("title").text("ZIO Essentials").build)
.child(XmlBuilder.element("price").text("39.99").build)
.build,
XmlPatch.Position.AppendChild
)

val updated = patch(xml)

Real-World Examples​

Learn by examining practical examples of XML codecs in action:

RSS Feed Parsing​

Parse RSS feeds by defining a schema and decoding XML:

import zio.blocks.schema._
import zio.blocks.schema.xml._

@xmlNamespace(uri = "http://www.rss.org/", prefix = "rss")
case class Item(
@xmlAttribute() guid: String,
title: String,
link: String,
pubDate: String,
description: String
)

@xmlNamespace(uri = "http://www.rss.org/", prefix = "rss")
case class Channel(
title: String,
link: String,
description: String,
items: List[Item]
)

object Channel {
implicit val itemSchema: Schema[Item] = Schema.derived
implicit val schema: Schema[Channel] = Schema.derived
}

// Parse RSS feed from string
val feedXml = """<rss:Channel xmlns:rss="http://www.rss.org/">
<title>Tech Blog</title>
<link>https://example.com</link>
<description>Latest tech articles</description>
<items>
<Item guid="1">
<title>Functional Programming in Scala</title>
<link>https://example.com/fp</link>
<pubDate>2024-01-01</pubDate>
<description>Deep dive into FP concepts</description>
</Item>
</items>
</rss:Channel>"""

val codec = Schema[Channel].derive(XmlFormat)
val result: Either[SchemaError, Channel] = codec.decode(feedXml)

Atom Feed with Advanced Features​

Work with Atom feeds using attributes, multiple entries, and custom configurations:

import zio.blocks.schema._
import zio.blocks.schema.xml._

@xmlNamespace(uri = "http://www.w3.org/2005/Atom", prefix = "atom")
case class Entry(
@xmlAttribute() id: String,
title: String,
author: String,
updated: String,
@xmlAttribute("href") link: String
)

@xmlNamespace(uri = "http://www.w3.org/2005/Atom", prefix = "atom")
case class Feed(
@xmlAttribute() id: String,
title: String,
updated: String,
entries: List[Entry]
)

object Feed {
implicit val entrySchema: Schema[Entry] = Schema.derived
implicit val schema: Schema[Feed] = Schema.derived
}

// Encode feed to XML with custom formatting
val feed = Feed(
id = "urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6",
title = "Example Feed",
updated = "2024-01-01T18:30:02Z",
entries = List(
Entry(
id = "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a",
title = "Atom-Powered Robots Run Amok",
author = "John Doe",
updated = "2024-01-01T18:30:02Z",
link = "http://example.org/2024/01/entry"
)
)
)

val codec = Schema[Feed].derive(XmlFormat)
val xmlOutput = codec.encodeToString(feed, WriterConfig.pretty)

Sitemap XML​

Generate sitemap XML with URLs and optional metadata fields:

import zio.blocks.schema._
import zio.blocks.schema.xml._

case class Url(
loc: String,
lastmod: Option[String],
changefreq: Option[String],
priority: Option[Double]
)

case class Urlset(
urls: List[Url]
)

object Urlset {
implicit val urlSchema: Schema[Url] = Schema.derived
implicit val schema: Schema[Urlset] = Schema.derived
}

// Build and encode sitemap
val sitemap = Urlset(List(
Url("https://example.com", Some("2024-01-01"), Some("monthly"), Some(1.0)),
Url("https://example.com/about", Some("2024-01-01"), Some("monthly"), Some(0.8)),
Url("https://example.com/contact", None, Some("monthly"), Some(0.5))
))

val codec = Schema[Urlset].derive(XmlFormat)
val sitemapXml = codec.encodeToString(sitemap, WriterConfig(
indentStep = 2,
includeDeclaration = true
))