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
- Output types: String, Streaming, and Document
- Dependencies
- Usage
- More examples
- Project content
importorg.encalmo.writer.xml.XmlWritercaseclassAddress(
street: String,
city: String,
postcode: String
)
caseclassEmployee(
name: String,
age: Int,
email: Option[String],
addresses: List[Address],
active: Boolean
)
valentity=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
)
valxml=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:
{
valbuilder: org.encalmo.writer.xml.XmlOutputBuilder= ...
builder.appendElementStart("Employee", immutable.Nil)
defwriteCaseClassToXml_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")
}
defwriteCaseClassToXml_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 {
casestring: scala.Some[scala.Predef.String] =>
builder.appendElementStart("email")
builder.appendText(string.value)
builder.appendElementEnd("email")
case scala.None=>
()
}
builder.appendElementStart("addresses")
valaddressesIterator: scala.collection.Iterator[Address] = (employee.addresses: scala.collection.Iterable[Address]).iterator
while (addressesIterator.hasNext) {
valaddressItem: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.
caseclassFoo(bar: String)
valentity=Foo("HELLO")
// <Foo><bar>HELLO</bar></Foo>valxml1=XmlWriter.writeIndented(entity) // <Example><bar>HELLO</bar></Example>valxml2=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
caseclassTool(name: String, weight: Double)
caseclassToolBox(hammer: Tool, screwdriver: Tool)
valentity=ToolBox(
hammer =Tool(name ="Hammer", weight =10.0),
screwdriver =Tool(name ="Screwdriver", weight =2.0)
)
valxml=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>| Output Type | Use Case | Example API |
|---|---|---|
String | Quick serialization, logs, tests, small data | XmlWriter.writeIndented(entity) |
| Stream | Large data, file/network/stream, low memory | XmlWriter.streamIndented(...) |
| Document | Java/Scala XML interop, DOM manipulation | XmlWriter.writeToDocument(entity) |
Choose the output option that matches your workflow—converting between them is possible, but choosing the most direct is typically more efficient.
The default for most APIs. Methods like XmlWriter.writeIndented and XmlWriter.writeCompact return the XML as a String for easy inspection, logging, or further in-memory processing.
valxml:String=XmlWriter.writeIndented(entity)For efficient and memory-safe writing of large or unknown-size documents, you can direct output straight to an OutputStream (e.g., file, network socket):
importjava.io.FileOutputStreamvalout=newFileOutputStream("output.xml")
XmlWriter.streamIndented(entity, out, addXmlDeclaration =true)
out.close()or for compact (single-line) XML:
XmlWriter.streamCompact(entity, out, addXmlDeclaration =false)For integration with Java XML tools or advanced in-memory XML manipulation, you can serialize to a org.w3c.dom.Document:
valdocument: org.w3c.dom.Document=XmlWriter.writeToDocument(entity)This DOM-based output allows you to use the rich Java XML ecosystem for further processing, validation, or transformation (for example, using XPath or XSLT).
If you need to generate an XML document with a specific default namespace (e.g., for standards compliance or interoperability), use XmlWriter.writeDocumentWithNamespace. This creates a DOM document with the namespace applied to the root element and all descendants where appropriate.
importorg.encalmo.writer.xml.XmlWritercaseclassPerson(
name: String,
age: Int// ... other fields ...
)
valperson=Person(
name ="John Doe",
age =30// ... other values ...
)
valnamespace="http://example.com/person"// Produce a DOM Document (org.w3c.dom.Document) with namespacevaldoc: org.w3c.dom.Document=XmlWriter.writeDocumentWithNamespace(person, namespace)
// To convert the document to a String for output or inspection:valxmlString= org.encalmo.writer.xml.XmlUtils.toXmlString(doc)
println(xmlString)This produces output like:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Personxmlns="http://example.com/person">
<name>John Doe</name>
<age>30</age>
<!-- ... other fields ... -->
</Person>If you need to produce XML with multiple namespaces mapped to different prefixes, you can use XmlWriter.writeDocumentWithNamespaceMapping. This method allows you to specify a default namespace and any number of additional namespace prefixes and URIs. All child elements will use the namespace of the parent element, unless the child element gets a new namespace from the mapping.
importorg.encalmo.writer.xml.XmlWriterimportorg.encalmo.writer.xml.annotation.*caseclassBook(
title: String,
author: String
)
caseclassLibrary(
@xmlAttribute libraryId: String,
name: String,
@xmlItemTag("Book") books: List[Book]
)
vallibrary=Library(
libraryId ="lib123",
name ="City Library",
books =List(
Book("Programming Scala", "Dean Wampler"),
Book("Functional Programming in Scala", "Paul Chiusano")
)
)
valnsMapping=Map(
"Library"-> ("","http://example.com/library"), // default namespace"Book"-> ("bk","http://example.com/book")
)
valdoc: org.w3c.dom.Document=XmlWriter.writeDocumentWithNamespaceMapping(library, nsMapping)
// Convert the document to a String for display or output:valxmlString= org.encalmo.writer.xml.XmlUtils.toXmlString(doc)
println(xmlString)This will produce output similar to:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Libraryxmlns="http://example.com/library"xmlns:bk="http://example.com/book"libraryId="lib123">
<name>City Library</name>
<books>
<bk:Book>
<bk:title>Programming Scala</bk:title>
<bk:author>Dean Wampler</bk:author>
</bk:Book>
<bk:Book>
<bk:title>Functional Programming in Scala</bk:title>
<bk:author>Paul Chiusano</bk:author>
</bk:Book>
</books>
</Library>This approach is useful for generating XML that integrates with schemas or APIs requiring multiple namespaces, allowing you to fully control the output format.
- Scala >= 3.7.4
- org.encalmo type-tree-visitor 0.10.0
Use with SBT
libraryDependencies += "org.encalmo" %% "xmlwriter" % "0.18.0"
or with SCALA-CLI
//> using dep org.encalmo::xmlwriter:0.18.0
Example with nested case classes and optional fields:
importorg.encalmo.writer.xml.XmlWritercaseclassAddress(
street: String,
city: String,
postcode: String,
country: Option[String] =None
)
caseclassCompany(
name: String,
address: Address
)
caseclassEmployee(
name: String,
age: Int,
email: Option[String],
address: Option[Address],
company: Option[Company]
)
valemployee=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)valxml: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 annotationsimportorg.encalmo.writer.xml.XmlWriterimportorg.encalmo.writer.xml.annotation.{xmlAttribute, xmlItemTag, xmlTag}
caseclassTag(
@xmlAttribute name: String,
value: String
)
@xmlTag("Bookshelf")
caseclassLibrary(
@xmlAttribute libraryId: String,
name: String,
@xmlItemTag("Book") books: List[Book]
)
caseclassBook(
@xmlAttribute isbn: String,
title: String,
author: String,
tags: List[Tag]
)
vallibrary=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")
)
)
)
)
valxml:String=XmlWriter.writeIndented(library)
println(xml)Output:
<?xml version='1.0' encoding='UTF-8'?>
<BookshelflibraryId="lib123">
<name>City Library</name>
<books>
<Bookisbn="978-3-16-148410-0">
<title>Programming Scala</title>
<author>Dean Wampler</author>
<tags>
<Tagname="Scala">Functional</Tag>
<Tagname="Programming">JVM</Tag>
</tags>
</Book>
<Bookisbn="978-1-61729-065-7">
<title>Functional Programming in Scala</title>
<author>Paul Chiusano</author>
<tags>
<Tagname="Scala">FP</Tag>
<Tagname="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
├── XmlUtils.scala
├── XmlWriter.scala
├── XmlWriterMacro.scala
├── XmlWriterMacroVisitor.scala
└── XmlWriterSpec.test.scala