https://github.com/encalmo/xmlwriter
Macro-powered fast XML serialization library for Scala 3.
https://github.com/encalmo/xmlwriter
macros scala3 serialization xml
Last synced: 4 months ago
JSON representation
Macro-powered fast XML serialization library for Scala 3.
- Host: GitHub
- URL: https://github.com/encalmo/xmlwriter
- Owner: encalmo
- License: mit
- Created: 2026-01-27T00:39:38.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-02-26T21:56:57.000Z (4 months ago)
- Last Synced: 2026-02-27T03:43:32.347Z (4 months ago)
- Topics: macros, scala3, serialization, xml
- Language: Scala
- Homepage: https://encalmo.github.io/xmlwriter/
- Size: 232 KB
- Stars: 9
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
  
# xmlwriter
Macro-powered fast and easy XML serialization library for Scala 3.
## Table of contents
- [Example usage](#example-usage)
- [Outstanding features](#outstanding-features)
- [Scala types supported directly without the need for typeclass derivation](#scala-types-supported-directly-without-the-need-for-typeclass-derivation)
- [Supported Java types](#supported-java-types)
- [Supported annotations](#supported-annotations)
- [Key abstractions](#key-abstractions)
- [How do we tag elements?](#how-do-we-tag-elements?)
- [Root element tag](#root-element-tag)
- [Nested elements](#nested-elements)
- [Dependencies](#dependencies)
- [Usage](#usage)
- [More examples](#more-examples)
- [Project content](#project-content)
## Example usage
```scala
import org.encalmo.writer.xml.XmlWriter
case class Address(
street: String,
city: String,
postcode: String
)
case class Employee(
name: String,
age: Int,
email: Option[String],
addresses: List[Address],
active: Boolean
)
val entity = Employee(
name = "John Doe",
age = 30,
email = Some("john.doe@example.com"),
addresses = List(
Address(street = "123 Main St", city = "Anytown", postcode = "12345"),
Address(street = "456 Back St", city = "Downtown", postcode = "78901")
),
active = true
)
val xml = XmlWriter.writeIndented(entity)
println(xml)
```
Output:
```xml
John Doe
30
john.doe@example.com
123 Main St
Anytown
12345
456 Back St
Downtown
78901
true
```
The example above produces the following code after macro expansion:
```scala
{
val builder: org.encalmo.writer.xml.XmlOutputBuilder = ...
builder.appendElementStart("Employee", immutable.Nil)
def writeCaseClassToXml_Address(address: Address): scala.Unit = {
builder.appendElementStart("street")
builder.appendText(address.street)
builder.appendElementEnd("street")
builder.appendElementStart("city")
builder.appendText(address.city)
builder.appendElementEnd("city")
builder.appendElementStart("postcode")
builder.appendText(address.postcode)
builder.appendElementEnd("postcode")
}
def writeCaseClassToXml_Employee(employee: Employee): scala.Unit = {
builder.appendElementStart("name")
builder.appendText(employee.name)
builder.appendElementEnd("name")
builder.appendElementStart("age")
builder.appendText(employee.age.toString())
builder.appendElementEnd("age")
employee.email match {
case string: scala.Some[scala.Predef.String] =>
builder.appendElementStart("email")
builder.appendText(string.value)
builder.appendElementEnd("email")
case scala.None =>
()
}
builder.appendElementStart("addresses")
val addressesIterator: scala.collection.Iterator[Address] = (employee.addresses: scala.collection.Iterable[Address]).iterator
while (addressesIterator.hasNext) {
val addressItem: Address = addressesIterator.next()
builder.appendElementStart("Address", immutable.Nil)
writeCaseClassToXml_Address(addressItem)
builder.appendElementEnd("Address")
()
}
builder.appendElementEnd("addresses")
builder.appendElementStart("active")
builder.appendText(employee.active.toString())
builder.appendElementEnd("active")
}
writeCaseClassToXml_Employee(entity)
builder.appendElementEnd("Employee")
}
```
## Outstanding features
- **Generates highly performant low-level code**
- Supports **field, value, case, and type annotations** enabling fine-tuning of the resulting XML,
- Supports **custom tag and attribute name transformation** (e.g., snake_case, kebab-case, upper/lower case, etc),
- **Indented or compact XML output** with pluggable output builders (including streaming),
- Automatic **escaping of text** (element and attribute content) to produce well-formed XML.
- Extensible to custom types via **typeclass** instances,
- Can automatically **derive** `XmlWriter` typeclass if requested,
- Invokes `toString()` as a **fallback** strategy when type is not supported directly or does not have an XmlWriter instance in scope.
- Decouples data structure traversal (`XmlWriter`) from output assembly (`XmlOutputBuilder`)
## Scala types supported directly without the need for typeclass derivation
- **Case classes** and nested case classes (including recursive, deeply nested types)
- **Enums and sealed trait hierarchies**
- **Tuples**: e.g. `(A, B)`, `(A, B, C)` etc.
- **Named tuples**: `(a: A, b: B)`
- **Instances of `Selectable` with a `Fields` type**: serialization for structural types and objects extending `Selectable` with a `Fields` member type
- **Opaque types with an upper bound**
- **Iterable[T]** collections and **Array[T]**
- **Option[T]**: (properly serializes presence or absence)
- **Either[T]**
- All standard **Scala primitive types**: `Int`, `Long`, `Double`, `Float`, `Boolean`, `Char`, `Short`, `Byte` and **`String`**
- **Big number types**: `BigInt`, `BigDecimal`
## Supported Java types
- **Java boxed primitives:** `java.lang.Integer`, `java.lang.Long`, `java.lang.Double`, etc.
- **Java records**
- **Java enums**
- **Java iterables:** support for `java.util.List`, `java.util.Set`, and other iterables
- **Java maps:** support for `java.util.Map` and subclasses
## Supported annotations
- All annotations are defined in `org.encalmo.writer.xml.annotation`.
- Annotations can be placed on types, fields, values and enum cases, on case class fields or sealed trait members.
- Custom tag and attribute names are only required when you want to override defaults.
| Annotation | Description |
|-----------------------|-------------------------------------------------------------------------------------------------------|
| `@xmlAttribute` | Marks the target to be serialized as an XML attribute of the enclosing element rather than as a child. |
| `@xmlContent` | Marks target as the content (text value) of the XML element instead of a tag or attribute. |
| `@xmlTag` | Sets a custom XML tag or attribute name for this target (overrides the target name in serialization). |
| `@xmlAdditionalTag` | Annotation to wrap value in and additional XML element |
| `@xmlTagLabelAndType` | Annotation to mandate nested tag elements for a field: <field><type> ... </type></field> |
| `@xmlItemTag` | Annotation to define the name of the XML element wrapping each item in an array or collection. This will override custom names of the items in the collection. |
| `@xmlAdditionalItemTag` | Annotation to define the name of the XML element additionally wrapping each item in an array or collection. This will NOT override custom names of the items in the collection. |
| `@xmlNoItemTags` | Prevents wrapping each collection element in an extra XML tag; all items are added directly. |
| `@xmlValue` | Defines a static value for an element, useful for enum cases |
| `@xmlValueSelector` | Selects which member/field/property from a nested type is used as the value/text for this element. |
| `@xmlEnumCaseValuePlain` | Annotation to force writing the enum case value as plain text, without wrapping it in a tag. |
## Key abstractions
- object [`XmlWriter`](XmlWriter.scala) provides the main user-facing API, a host of methods to serialize data types to XML,
- trait `XmlWriter[T]` defines typeclass interface,
- trait [`XmlOutputBuilder`](XmlOutputBuilder.scala) defines low-level API for constructing XML output,
- object `XmlOutputBuilder` provides a set of default implementations of `XmlOutputBuilder` trait producing indented or compact format, building a `String` or writing directly to the `java.io.OutputStream`
## How do we tag elements?
### Root element tag
Root tag can be either provided by the user or derived from the type name.
```scala
case class Foo(bar: String)
val entity = Foo("HELLO")
// HELLO
val xml1 = XmlWriter.writeIndented(entity)
// HELLO
val xml2 = XmlWriter.writeIndentedUsingRootTagName("Example", entity, addXmlDeclaration = false)
```
## Nested elements
Nested elements borrow tag name either from:
- field name of case classes, selectables or records
- enum case name or value
- declared type name (including type aliases and opaque types)
- keys of the map
- @xmlTag and @xmlItemTag annotations
```scala
case class Tool(name: String, weight: Double)
case class ToolBox(hammer: Tool, screwdriver: Tool)
val entity =
ToolBox(
hammer = Tool(name = "Hammer", weight = 10.0),
screwdriver = Tool(name = "Screwdriver", weight = 2.0)
)
val xml = XmlWriter.writeIndented(entity)
println(xml)
```
```xml
Hammer
10.0
Screwdriver
2.0
```
## Dependencies
- [Scala](https://www.scala-lang.org) >= 3.7.4
- org.encalmo [**macro-utils** 0.11.0](https://central.sonatype.com/artifact/org.encalmo/macro-utils_3)
## Usage
Use with SBT
libraryDependencies += "org.encalmo" %% "xmlwriter" % "0.13.0"
or with SCALA-CLI
//> using dep org.encalmo::xmlwriter:0.13.0
## More examples
Example with nested case classes and optional fields:
```scala
import org.encalmo.writer.xml.XmlWriter
case class Address(
street: String,
city: String,
postcode: String,
country: Option[String] = None
)
case class Company(
name: String,
address: Address
)
case class Employee(
name: String,
age: Int,
email: Option[String],
address: Option[Address],
company: Option[Company]
)
val employee = Employee(
name = "Alice Smith",
age = 29,
email = Some("alice.smith@company.com"),
address = Some(
Address(
street = "456 Market Ave",
city = "Metropolis",
postcode = "90210",
country = None
)
),
company = Some(
Company(
name = "Acme Widgets Inc.",
address = Address(
street = "123 Corporate Plaza",
city = "Metropolis",
postcode = "90211",
country = Some("USA")
)
)
)
)
// Serialize as indented XML (with XML declaration)
val xml: String = XmlWriter.writeIndented(employee)
println(xml)
```
Output:
```xml
Alice Smith
29
alice.smith@company.com
456 Market Ave
Metropolis
90210
Acme Widgets Inc.
123 Corporate Plaza
Metropolis
90211
USA
```
```scala
// Example: Serialize a case class with collections and XML annotations
import org.encalmo.writer.xml.XmlWriter
import org.encalmo.writer.xml.annotation.{xmlAttribute, xmlItemTag, xmlTag}
case class Tag(
@xmlAttribute name: String,
value: String
)
@xmlTag("Bookshelf")
case class Library(
@xmlAttribute libraryId: String,
name: String,
@xmlItemTag("Book") books: List[Book]
)
case class Book(
@xmlAttribute isbn: String,
title: String,
author: String,
tags: List[Tag]
)
val library = Library(
libraryId = "lib123",
name = "City Library",
books = List(
Book(
isbn = "978-3-16-148410-0",
title = "Programming Scala",
author = "Dean Wampler",
tags = List(
Tag(name = "Scala", value = "Functional"),
Tag(name = "Programming", value = "JVM")
)
),
Book(
isbn = "978-1-61729-065-7",
title = "Functional Programming in Scala",
author = "Paul Chiusano",
tags = List(
Tag(name = "Scala", value = "FP"),
Tag(name = "Education", value = "Advanced")
)
)
)
)
val xml: String = XmlWriter.writeIndented(library)
println(xml)
```
Output:
```xml
City Library
Programming Scala
Dean Wampler
Functional
JVM
Functional Programming in Scala
Paul Chiusano
FP
Advanced
```
## Project content
```
├── .github
│ └── workflows
│ ├── pages.yaml
│ ├── release.yaml
│ └── test.yaml
│
├── .gitignore
├── .scalafmt.conf
├── annotation.scala
├── ExampleModel.test.scala
├── ExampleModelSpec.test.scala
├── LICENSE
├── Order.java
├── project.scala
├── README.md
├── SimpleTypeTreeVisitor.scala
├── Status.java
├── TagName.scala
├── test.sh
├── TestData.test.scala
├── TestModel.test.scala
├── TypeTreeIterator.scala
├── TypeTreeVisitor.scala
├── XmlOutputBuilder.scala
├── XmlWriter.scala
├── XmlWriterMacro.scala
├── XmlWriterMacroVisitor.scala
└── XmlWriterSpec.test.scala
```