Skip to content
mandubian edited this page Jul 13, 2012 · 2 revisions

XML Scala serialization/deserialization

The concepts

We use the same design as Play2 Json serialization/deserialization based on implicit typeclasses. This mechanism is very clean, robust and generic. Please note that implicits are NOT used as implicit CONVERSIONS which are often tricky and sometimes dangerous!

Imagine you want to map this XML to a case class

val fooXml = <foo>
    <id>1234</id>
    <name>brutus</name>
    <age>23</age>
</foo>

You want to map it to:

case class Foo(id: Long, name: String, age: Option[Int])
Foo(id=1234L, name="brutus", age=Some(23))

Please note:

  • case class is a sample but the mechanism also works with any structure in Scala such as tuples
  • age field is Option[Int] meaning it might not appear in the XML (None in this case)

So how would you write that with xmlsoap ersatz?

import play2.tools.xml._
import play2.tools.xml.DefaultImplicits._
[...]
val foo = EXML.fromXML[Foo](fooXml)
assert(foo == Foo(1234L, "brutus", Some(23)))

val fooXml2 = EXML.toXML(foo)
assert(fooXml2 == fooXml)

As you may imagine, this is not so simple as fromXML/toXML signatures are the following:

object EXML {
    def toXML[T](t: T, base: xml.NodeSeq = xml.NodeSeq.Empty)(implicit w: XMLWriter[T]): xml.NodeSeq
    def fromXML[T](x: xml.NodeSeq)(implicit r: XMLReader[T]): Option[T] 
}

You can see the implicit typeclasses XMLReader/XMLWriter which define the mapper to/from XML to your case class. So in order EXML.fromXML/EXML.toXML to work properly, you should define an implicit XML reader/writer for your specific structure in your scope. It can be done at once by extending XMLFormatter[T]:

trait XMLFormatter[T] extends XMLReader[T] with XMLWriter[T] {
    def read(x: xml.NodeSeq): Option[T]
    def write(f: T, base: xml.NodeSeq): xml.NodeSeq
}

Defining the implicit for your case class

implicit object FooXMLF extends XMLFormatter[Foo] {
  def read(x: xml.NodeSeq): Option[Foo] = {
    for( 
      id <- EXML.fromXML[Long](x \ "id");
      name <- EXML.fromXML[String](x \ "name");
      age <- EXML.fromXML[Option[Int]](x \ "age")
    ) yield(Foo(id, name, age))
  }

  def write(f: Foo, base: xml.NodeSeq): xml.NodeSeq = {
    <foo>
      <id>{ f.id }</id>
      <name>{ f.name }</name>
      { EXML.toXML(f.age, <age/>) }
    </foo>
  }
} 

You may think this is a bit tedious to write but this is quite easy after a few tries and the most important:

This mechanism provides a very precise and simple control on what you want to do.

Please note:

  • the write function uses Scala XML literals simply.

  • the implicit is important: you can declare it once in your scope and that's all.

  • the age field in write requires a special syntax:

    { EXML.toXML(f.age, <age/>) } means: <age/> is used as the base node and will generate following XML:

    • if age is Some(23): <age>23</age>
    • if age is None: <age xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true" /> (standards defines this but you can redefine it as you want)
  • the base field in write function can be used to pass a parent context node to the writer. It is helpful in case you need to add attributes to parent node for ex as in the case of age: Option[Int] field.

But don't forget that Scala XML nodes are immutable and that you can just copy the nodes you pass to a function.

  • the for-comprehension is just a shortcut but you could write it using flatMap/map also: For ex:

    def read(x: xml.NodeSeq): Option[Foo] = { EXML.fromXML[Long](x \ "id").flatMap{ id => EXML.fromXML[String](x \ "name").flatMap { name => EXML.fromXML[Int](x \ "age").flatMap{ age => Foo(id, name, age) } } } }

Finally the complete code

import play2.tools.xml._
import play2.tools.xml.DefaultImplicits._

implicit object FooXMLF extends XMLFormatter[Foo] {
  def read(x: xml.NodeSeq): Option[Foo] = {
    for( 
      id <- EXML.fromXML[Long](x \ "id");
      name <- EXML.fromXML[String](x \ "name");
      age <- EXML.fromXML[Option[Int]](x \ "age");
    ) yield(Foo(id, name, age))
  }

  def write(f: Foo, base: xml.NodeSeq): xml.NodeSeq = {
    <foo>
      <id>{ f.id }</id>
      <name>{ f.name }</name>
      { EXML.toXML(f.age, <age/>) }
    </foo>
  }
}

val foo = EXML.fromXML[Foo](fooXml)
assert(foo == Foo(1234L, "albert", 23)

val fooXml = EXML.toXML(foo)
assert(fooXml == fooXml)
Clone this wiki locally