How to create JSON strings from Scala classes that have collections fields

This is an excerpt from the Scala Cookbook (partially modified for the internet). This is Recipe 15.2, “How to create a JSON String from Scala classes that have collections.”

Problem

You want to generate a JSON representation of a Scala object that contains one or more collections, such as a Person class that has a list of friends or addresses.

Solution

Once classes start containing collections, converting them to JSON becomes more difficult. In this situation, I prefer to use the Lift-JSON domain-specific library (DSL) to generate the JSON.

Lift-JSON version 1

The Lift-JSON library uses its own DSL for generating JSON output from Scala objects. As shown in the previous recipe, this isn’t necessary for simple objects, but it is necessary once objects become more complex, specifically once they contain collections. The benefit of this approach is that you have complete control over the JSON that is generated.

The following example shows how to generate a JSON string for a Person class that has a friends field defined as List[Person]:

import net.liftweb.json._
import net.liftweb.json.JsonDSL._

case class Person(name: String, address: Address) {
  var friends = List[Person]()
}

case class Address(city: String, state: String)
object LiftJsonListsVersion1 extends App {
  //import net.liftweb.json.JsonParser._
  implicit val formats = DefaultFormats
  val merc = Person("Mercedes", Address("Somewhere", "KY"))
  val mel = Person("Mel", Address("Lake Zurich", "IL"))
  val friends = List(merc, mel)
  val p = Person("Alvin Alexander", Address("Talkeetna", "AK"))
  p.friends = friends
  // define the json output
  val json =
    ("person" ->
      ("name" -> p.name) ~
      ("address" ->
        ("city" -> p.address.city) ~
        ("state" -> p.address.state)) ~
      ("friends" ->
        friends.map { f =>
          ("name" -> f.name) ~
          ("address" ->
            ("city" -> f.address.city) ~
            ("state" -> f.address.state))
        })
    )
  println(pretty(render(json)))
}

The JSON output from this code looks like this:

{
  "person":{
    "name":"Alvin Alexander",
    "address":{
      "city":"Talkeetna",
      "state":"AK"
    },
    "friends":[{
      "name":"Mercedes",
      "address":{
        "city":"Somewhere",
        "state":"KY"
      }
    },{
      "name":"Mel",
      "address":{
        "city":"Lake Zurich",
        "state":"IL"
      }
    }]
  }
}

The JSON-generating code is shown after the “define the json output” comment, and is repeated here:

val json =
  ("person" ->
    ("name" -> p.name) ~
    ("address" ->
      ("city" -> p.address.city) ~
      ("state" -> p.address.state)) ~
    ("friends" ->
      friends.map { f =>
        ("name" -> f.name) ~
        ("address" ->
          ("city" -> f.address.city) ~
          ("state" -> f.address.state))
      })
  )

As you can see, Lift uses a custom DSL to let you generate the JSON, and also have control over how the JSON is generated (as opposed to using reflection to generate the JSON). Although you’ll want to read the details of the DSL to take on more difficult tasks, the basics are straightforward.

The first thing to know is that any Tuple2 generates a JSON field, so a code snippet like ("name" -> p.name) produces this output:

"name":"Alvin Alexander"

The other important thing to know is that the ~ operator lets you join fields. You can see from the example code and output how it works.

You can also refer to objects and methods when generating the JSON. You can see this in sections of the code like p.address.city and friends.map { f =>.

Writing JSON-generating code like this feels just like writing other Scala code.

Lift-JSON Version 2

As your classes grow, creating a larger JSON generator in one variable becomes hard to read and maintain. Fortunately, with the Lift-JSON DSL you can break your JSON-generating code down into small chunks to keep the code maintainable. The following code achieves the same result as the previous example, but I’ve broken the JSON-generating code down into small methods that are easier to maintain and reuse:

import net.liftweb.json._
import net.liftweb.json.JsonDSL._

object LiftJsonListsVersion2 extends App {
  val merc = Person("Mercedes", Address("Somewhere", "KY"))
  val mel = Person("Mel", Address("Lake Zurich", "IL"))
  val friends = List(merc, mel)
  val p = Person("Alvin Alexander", Address("Talkeetna", "AK"))
  p.friends = friends
  val json =
    ("person" ->
      ("name" -> p.name) ~
      getAddress(p.address) ~
        getFriends(p)
    )
  println(pretty(render(json)))
  def getFriends(p: Person) = {
    ("friends" ->
      p.friends.map { f =>
        ("name" -> f.name) ~
        getAddress(f.address)
      })
  }
  def getAddress(a: Address) = {
    ("address" ->
      ("city" -> a.city) ~
      ("state" -> a.state))
  }
}

case class Person(name: String, address: Address) {
  var friends = List[Person]()
}

case class Address(city: String, state: String)

As shown, this approach lets you create methods that can be reused. The getAddress method, for instance, is called several times in the code.

Discussion

As shown in Recipe 15.1, Gson works via reflection, and it works well for simple classes. However, I’ve found it to be harder to use when your classes have certain collections. For instance, the following code works fine when the list of friends is defined as an Array[Person]:

import com.google.gson.Gson
import com.google.gson.GsonBuilder

case class Person(name: String, address: Address) {
  var friends: Array[Person] = _
}

case class Address(city: String, state: String)

/**
  * This approach works with Array.
  */
object GsonWithArray extends App {
  val merc = Person("Mercedes", Address("Somewhere", "KY"))
  val mel = Person("Mel", Address("Lake Zurich", "IL"))
  val friends = Array(merc, mel)
  val p = Person("Alvin Alexander", Address("Talkeetna", "AK"))
  p.friends = friends
  val gson = (new GsonBuilder()).setPrettyPrinting.create
  println(gson.toJson(p))
}

Because a Scala Array is backed by a Java array, that code works well, generating JSON output that is similar to Lift-JSON. However, if you change the Array[Person] to List[Person], Gson removes the list of friends from the output:

{
  "name": "Alvin Alexander",
  "address": {
    "city": "Talkeetna",
    "state": "AK"
  },
  "friends": {}
}

Changing the Array to an ArrayBuffer also causes problems and exposes the internal implementation of an ArrayBuffer:

{
  "name": "Alvin Alexander",
  "address": {
    "city": "Talkeetna",
    "state": "AK"
  },
  "friends": {
    "initialSize": 16,
    "array": [
      {
        "name": "Mercedes",
        "address": {
          "city": "Somewhere",
          "state": "KY"
        }
      },
      {
        "name": "Mel",
        "address": {
          "city": "Lake Zurich",
          "state": "IL"
        }
      },
      null,  // this line is repeated 13 more times
      ...
      ...
      null
    ],
    "size0": 2
  }
}

An ArrayBuffer begins with 16 elements, and when Gson generates the JSON for the list of friends, it correctly includes the two friends, but then outputs the word null 14 times, along with including the other output shown.

If you like the idea of generating JSON from your code using reflection, see the Gson User Guide link in the See Also section for information on how to try to resolve these issues by writing custom serializers (creating a JSON string from an object) and deserializers (creating an object from a JSON string).

See Also