Skip to content

Repository files navigation

GitHubMaven Central VersionScaladoc

xmlwriter

Macro-powered fast and easy XML serialization library for Scala 3.

Table of contents

Example usage

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")
}

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 deriveXmlWriter 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.
AnnotationDescription
@xmlAttributeMarks the target to be serialized as an XML attribute of the enclosing element rather than as a child.
@xmlContentMarks target as the content (text value) of the XML element instead of a tag or attribute.
@xmlTagSets a custom XML tag or attribute name for this target (overrides the target name in serialization).
@xmlAdditionalTagAnnotation to wrap value in and additional XML element
@xmlTagLabelAndTypeAnnotation to mandate nested tag elements for a field: <field><type> ... </type></field>
@xmlItemTagAnnotation 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.
@xmlAdditionalItemTagAnnotation 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.
@xmlNoItemTagsPrevents wrapping each collection element in an extra XML tag; all items are added directly.
@xmlValueDefines a static value for an element, useful for enum cases
@xmlValueSelectorSelects which member/field/property from a nested type is used as the value/text for this element.
@xmlEnumCaseValuePlainAnnotation to force writing the enum case value as plain text, without wrapping it in a tag.

Key abstractions

  • object XmlWriter provides the main user-facing API, a host of methods to serialize data types to XML,
  • trait XmlWriter[T] defines typeclass interface,
  • trait XmlOutputBuilder 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.

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

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 types: String, Streaming, and Document

Output TypeUse CaseExample API
StringQuick serialization, logs, tests, small dataXmlWriter.writeIndented(entity)
StreamLarge data, file/network/stream, low memoryXmlWriter.streamIndented(...)
DocumentJava/Scala XML interop, DOM manipulationXmlWriter.writeToDocument(entity)

Choose the output option that matches your workflow—converting between them is possible, but choosing the most direct is typically more efficient.

1. String output

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)

2. Streaming output

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)

3. Document output (DOM) without namespace

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).

4. Document output (DOM) with default namespace

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>

5. Document output (DOM) with multiple namespaces

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.

Dependencies

Usage

Use with SBT

libraryDependencies += "org.encalmo" %% "xmlwriter" % "0.18.0"

or with SCALA-CLI

//> using dep org.encalmo::xmlwriter:0.18.0

More examples

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>

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
├── Status.java
├── test.sh
├── TestData.test.scala
├── TestModel.test.scala
├── XmlOutputBuilder.scala
├── XmlUtils.scala
├── XmlWriter.scala
├── XmlWriterMacro.scala
├── XmlWriterMacroVisitor.scala
└── XmlWriterSpec.test.scala

About

Macro-powered fast XML serialization library for Scala 3.

Topics

Resources

Stars

13 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages