Serializing and deserializing XML in Scala

Problem: You want to convert the data in a Scala class to its equivalent XML, or convert an XML representation of a class to an instance of the class.

Solution

There are two primary approaches to solving this problem:

  • Convert the fields in your Scala class to and from XML using the techniques shown in previous recipes in this chapter, i.e., a “manual conversion” process.
  • Use a library like XStream to assist in the conversion process, writing converters as necessary.

This solution demonstrates both approaches.

1) The manual conversion process

The following code has a toXml method in the class to convert the fields in a class to XML, and a fromXml method in the object to convert an XML literal to the fields in a class, using a manual process. This example uses a Stock class, meant to hold data for an instance of a stock, such as Stock(“NFLX”,“Netflix”,165.50):

class Stock(var symbol: String, var businessName: String, var price: Double) {

  // (a) convert Stock fields to XML
  def toXml = {
    <stock>
      <symbol>{symbol}</symbol>
      <businessName>{businessName}</businessName>
      <price>{price}</price>
    </stock>
  }

  override def toString = 
    s"symbol: $symbol, businessName: $businessName, price: $price"

}

object Stock {

  // (b) convert XML to a Stock
  def fromXml(node: scala.xml.Node):Stock = {
    val symbol = (node \ "symbol").text
    val businessName = (node \ "businessName").text
    val price = (node \ "price").text.toDouble
    new Stock(symbol, businessName, price)
  }

}

The toXml method uses the variable substitution techniques shown in Recipe 16.2 of the Scala Cookbook, and the fromXml method uses XPath search techniques demonstrated in Recipes 16.4 through 16.6. The toXml method is declared in the class because it needs to be unique to each instance, but the fromXml method is declared in the object because it’s called as Stock.fromXml(theXml), like a static method in Java.

(Note: The businessName field isn’t necessary here, but is kept to make sure the stock symbols are understood.)

These methods are demonstrated with the following driver class:

object TestToFromXml extends App {

  // (a) convert a Stock to its XML representation  
  val aapl = new Stock("AAPL", "Apple", 600d)
  println(aapl.toXml)
  
  // (b) convert an XML representation to a Stock
  val googXml = <stock>
      <symbol>GOOG</symbol>
      <businessName>Google</businessName>
      <price>620.00</price>
    </stock>
   val goog = Stock.fromXml(googXml)
   println(goog)
}

Running the TestToFromXml object produces the following output:

<stock>
      <symbol>AAPL</symbol>
      <businessName>Apple</businessName>
      <price>600.0</price>
    </stock>

symbol: GOOG, businessName: Google, price: 620.0

This process is straightforward, and uses techniques demonstrated in other recipes in this chapter. The only thing new in this recipe is using the toDouble method in this line of code:

val price = (node \ "price").text.toDouble

You can use toDouble and all the related to* methods, because the text method returns a String, which you can manipulate in all the usual ways.

Although writing code like this is a manual process, the code can easily be generated from the class specification or the database using a “CRUD generator.”

2) Using a tool like XStream

Another solution to this problem is to use a library such as XStream, a popular Java library for serializing XML. Like other Java libraries, it works with Scala, though you’ll need to provide a “converter” to handle special situations and collections.

For instance, you might start with the following simple class:

// a "first attempt" example (has a few problems)

import scala.collection.mutable.ArrayBuffer
import com.thoughtworks.xstream._
import com.thoughtworks.xstream.io.xml.DomDriver

case class Topping (name: String)

case class Pizza(crustSize: Int, crustType: String) {
  val toppings = ArrayBuffer[Topping]()
  def addTopping(t: Topping) { toppings += t }
}

object Test extends App {

  val p = Pizza(14, "Thin")
  p.addTopping(Topping("cheese"))
  p.addTopping(Topping("sausage"))

  val xstream = new XStream(new DomDriver)
  val xml = xstream.toXML(p)
  println(xml)

}

After including the XStream jar in the project, running the Test object results in the following output, which has a few issues:

<Pizza>
  <crustSize>14</crustSize>
  <crustType>Thin</crustType>
  <toppings>
    <initialSize>16</initialSize>
    <array>
      <Topping>
        <name>cheese</name>
      </Topping>
      <Topping>
        <name>sausage</name>
      </Topping>
      <null/>
      <null/>
      <null/>
      ...
      (this goes on until 14 null fields are printed)
      ...
      <null/>
      <null/>
      <null/>
    </array>
    <size0>2</size0>
  </toppings>
</Pizza>

Issues in this output include:

  • The “Pizza” and “Topping” tags are capitalized.
  • All those <null/> tags generated from the ArrayBuffer[Topping].
  • The extra <array> and <size> tags.

To solve these problems, you need to create aliases to handle the capitalization problems, and write an XStream converter to handle the collection.

A converter class named net.mixedbits.tools.XStreamConversions provides a good start toward solving the collections problem. At the time of this writing it currently has one “import” bug that’s easily fixed, plus some deprecation issues.

After copying and pasting the XStreamConversions class into my project and fixing its import bug, the following program shows how to use it. In addition, the alias method calls show to convert the strings “Pizza” and “Topping” to lowercase:

// improved example

import scala.collection.mutable.ArrayBuffer
import com.thoughtworks.xstream._
import com.thoughtworks.xstream.io.xml.DomDriver
import com.thoughtworks.xstream.io.xml.StaxDriver

// added
import net.mixedbits.tools.XStreamConversions

case class Topping (name: String)

case class Pizza(crustSize: Int, crustType: String) {
  val toppings = ArrayBuffer[Topping]()
  def addTopping(t: Topping) { toppings += t }
}

object Test extends App {

  val p = Pizza(14, "Thin")
  p.addTopping(Topping("cheese"))
  p.addTopping(Topping("pepperoni"))

  // pass XStream into XStreamConversions
  val xstream = XStreamConversions(new XStream(new DomDriver()))

  // make Topping and Pizza lowercase
  xstream.alias("topping", classOf[Topping])
  xstream.alias("pizza", classOf[Pizza])

  val xml = xstream.toXML(p)
  println(xml)

}

Running this code results in the following (improved) output:

<pizza>
  <crustSize>14</crustSize>
  <crustType>Thin</crustType>
  <toppings>
    <topping>
      <name>cheese</name>
    </topping>
    <topping>
      <name>pepperoni</name>
    </topping>
  </toppings>
</pizza>

A library like XStream can really save time and effort if you need to serialize and deserialize a large number of classes.

See Also