Macro-powered fast and easy XML serialization library for Scala 3.
- Example usage
- Outstanding features
- Scala types supported directly without the need for typeclass derivation
- Supported Java types
- Supported annotations
- Key abstractions
- How do we tag elements?
- Nested elements
- Dependencies
- Usage
- More examples
- Project content
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 version='1.0' encoding='UTF-8'?>
<Employee>
<name>John Doe</name>
<age>30</age>
<email>john.doe@example.com</email>
<addresses>
<Address>
<street>123 Main St</street>
<city>Anytown</city>
<postcode>12345</postcode>
</Address>
<Address>
<street>456 Back St</street>
<city>Downtown</city>
<postcode>78901</postcode>
</Address>
</addresses>
<active>true</active>
</Employee>The example above produces the following code after macro expansion:
{
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")
}- 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
XmlWritertypeclass 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)
- 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
Selectablewith aFieldstype: serialization for structural types and objects extendingSelectablewith aFieldsmember 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,ByteandString - Big number types:
BigInt,BigDecimal
- 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.Mapand subclasses
- 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. |
- object
XmlWriterprovides the main user-facing API, a host of methods to serialize data types to XML, - trait
XmlWriter[T]defines typeclass interface, - trait
XmlOutputBuilderdefines low-level API for constructing XML output, - object
XmlOutputBuilderprovides a set of default implementations ofXmlOutputBuildertrait producing indented or compact format, building aStringor writing directly to thejava.io.OutputStream
Root tag can be either provided by the user or derived from the type name.
case class Foo(bar: String)
val entity = Foo("HELLO")
// <Foo><bar>HELLO</bar></Foo>
val xml1 = XmlWriter.writeIndented(entity)
// <Example><bar>HELLO</bar></Example>
val xml2 = XmlWriter.writeIndentedUsingRootTagName("Example", entity, addXmlDeclaration = false)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
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 version='1.0' encoding='UTF-8'?>
<ToolBox>
<hammer>
<name>Hammer</name>
<weight>10.0</weight>
</hammer>
<screwdriver>
<name>Screwdriver</name>
<weight>2.0</weight>
</screwdriver>
</ToolBox>- Scala >= 3.7.4
- org.encalmo type-tree-visitor 0.9.0
Use with SBT
libraryDependencies += "org.encalmo" %% "xmlwriter" % "0.16.0"
or with SCALA-CLI
//> using dep org.encalmo::xmlwriter:0.16.0
Example with nested case classes and optional fields:
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 version='1.0' encoding='UTF-8'?>
<Employee>
<name>Alice Smith</name>
<age>29</age>
<email>alice.smith@company.com</email>
<address>
<street>456 Market Ave</street>
<city>Metropolis</city>
<postcode>90210</postcode>
</address>
<company>
<name>Acme Widgets Inc.</name>
<address>
<street>123 Corporate Plaza</street>
<city>Metropolis</city>
<postcode>90211</postcode>
<country>USA</country>
</address>
</company>
</Employee>// 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 version='1.0' encoding='UTF-8'?>
<Bookshelf libraryId="lib123">
<name>City Library</name>
<books>
<Book isbn="978-3-16-148410-0">
<title>Programming Scala</title>
<author>Dean Wampler</author>
<tags>
<Tag name="Scala">Functional</Tag>
<Tag name="Programming">JVM</Tag>
</tags>
</Book>
<Book isbn="978-1-61729-065-7">
<title>Functional Programming in Scala</title>
<author>Paul Chiusano</author>
<tags>
<Tag name="Scala">FP</Tag>
<Tag name="Education">Advanced</Tag>
</tags>
</Book>
</books>
</Bookshelf>├── .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
├── Status.java
├── test.sh
├── TestData.test.scala
├── TestModel.test.scala
├── XmlOutputBuilder.scala
├── XmlWriter.scala
├── XmlWriterMacro.scala
├── XmlWriterMacroVisitor.scala
└── XmlWriterSpec.test.scala