Skip to content

Reflection based JSON Serialization

debasishg edited this page Feb 15, 2011 · 3 revisions

sjson supports serialization of Scala objects into JSON. It implements support for built in Scala structures like List, Map or String as well as custom objects.

Example: I have a Scala object as ..

val addr = Address("Market Street", "San Francisco", "956871")

where Address is a custom class defined by the user. Using sjson, I can store it as JSON and retrieve as plain old Scala object. Here’s the simple assertion that validates the invariant. Note that during de-serialziation, the class name is specified. Hence what it gives back is an instance of Address.

addr should equal(serializer.in[Address](serializer.out(addr)))

Note, that the class needs to have a default constructor. Otherwise the deserialization into the specified class will fail.

There are situations, particularly when writing generic persistence libraries in Akka, when the exact class is not known during de-serialization. Using sjson I can get it as AnyRef or Nothing ..

serializer.in[AnyRef](serializer.out(addr))

or just as ..

serializer.in(serializer.out(addr))

What you get back from is a JsValue, an abstraction of the JSON object model. For details of JsValue implementation, refer to dispatch-json that sjson uses as the underlying JSON parser implementation. Once I have the JsValue model, I can use use extractors to get back individual attributes ..

val a = serializer.in[AnyRef](serializer.out(addr))
val c = 'city ? str
val c(ci) = a
ci should equal("San Francisco")

val s = 'street ? str
val s(st) = a
st should equal("Market Street")

val z = 'zip ? str
val z(zp) = a
zp should equal("956871")

Serialization of Embedded Objects

sjson supports serialization of Scala objects that have other embedded objects. Suppose you have the following Scala classes .. Here Contact has an embedded Address Map ..

@BeanInfo case class Contact(name: String, 
  @(JSONTypeHint @field)(value = classOf[Address]) addresses: Map[String, Address]) {
  override def toString = "name = " + name + " addresses = " +
    addresses.map(a => a._1 + ":" + a._2.toString).mkString(",")
}
@BeanInfo case class Address(street: String, city: String, zip: String) {
  override def toString = "address = " + street + "/" + city + "/" + zip
}

With sjson, I can do the following:

val a1 = Address("Market Street", "San Francisco", "956871")
val a2 = Address("Monroe Street", "Denver", "80231")
val a3 = Address("North Street", "Atlanta", "987671")
val c = Contact("Bob", Map("residence" -> a1, "office" -> a2, "club" -> a3))

val co = serializer.out(c)

// with class specified
c should equal(serializer.in[Contact](co))

// no class specified
val a = serializer.in[AnyRef](co)

// extract name
val n = 'name ? str
val n(_name) = a
"Bob" should equal(_name)

// extract addresses
val addrs = 'addresses ? obj
val addrs(_addresses) = a

// extract residence from addresses
val res = 'residence ? obj
val res(_raddr) = _addresses

// make an Address bean out of _raddr
val address = JsBean.fromJSON(_raddr, Some(classOf[Address]))
a1 should equal(address)

object r { def ># [T](f: JsF[T]) = f(a.asInstanceOf[JsValue]) }

// still better: chain 'em up
"Market Street" should equal(
  (r ># { ('addresses ? obj) andThen ('residence ? obj) andThen ('street ? str) }))

Changing property names during serialization

@BeanInfo
case class Book(id: Number,
           title: String, @(JSONProperty @getter)(value = "ISBN") isbn: String) {

  override def toString = "id = " + id + " title = " + title + " isbn = " + isbn
}

When this will be serialized out, the property name will be changed.

val b = new Book(100, "A Beautiful Mind", "012-456372")
val jsBook = Js(JsBean.toJSON(b))
val expected_book_map = Map(
  JsString("id") -> JsNumber(100),
  JsString("title") -> JsString("A Beautiful Mind"),
  JsString("ISBN") -> JsString("012-456372")
)

Serialization with ignore properties

When serializing objects, some of the properties can be ignored declaratively. Consider the following class declaration:

@BeanInfo
case class Journal(id: BigDecimal,
                    title: String,
                    author: String,
                    @(JSONProperty @getter)(ignore = true) issn: String) {
  override def toString =
    "Journal: " + id + "/" + title + "/" + author +
      (issn match {
          case null => ""
          case _ => "/" + issn
        })
}

The annotation @JSONProperty can be used to selectively ignore fields. When I serialize a Journal object out and then back in, the content of issn field will be null.

it("should ignore issn field") {
    val j = Journal(100, "IEEE Computer", "Alex Payne", "012-456372")
    serializer.in[Journal](serializer.out(j)).asInstanceOf[Journal].issn should equal(null)
}

Similarly, we can ignore properties of an object only if they are null and not ignore otherwise. Just specify the annotation @JSONProperty as @(JSONProperty @getter)(ignoreIfNull = true).

Serialization with Type Hints for Generic Data Members

Consider the following Scala class:

@BeanInfo
case class Contact(name: String,
                   @(JSONTypeHint @field)(value = classOf[Address])
                   addresses: Map[String, Address]) {

  override def toString = "name = " + name + " addresses = " +
    addresses.map(a => a._1 + ":" + a._2.toString).mkString(",")
}

Because of erasure, you need to add the type hint declaratively through the annotation @JSONTypeHint that sjson will pick up during serialization. Now we can say:

val c = Contact("Bob", Map("residence" -> a1, "office" -> a2, "club" -> a3))
val co = serializer.out(c)
it("should give an instance of Contact") {
  c should equal(serializer.in[Contact](co))
}

With optional generic data members, we need to provide the hint to sjson through another annotation @OptionTypeHint.

@BeanInfo
case class ContactWithOptionalAddr(name: String,
                              @(JSONTypeHint @field)(value = classOf[Address])
                              @(OptionTypeHint @field)(value = classOf[Map[_,_]])
                              addresses: Option[Map[String, Address]]) {

  override def toString = "name = " + name + " " +
    (addresses match {
      case None => ""
      case Some(ad) => " addresses = " + ad.map(a => a._1 + ":" + a._2.toString).mkString(",")
    })
}

Serialization works ok with optional members annotated as above.

describe("Bean with optional bean member serialization") {
  it("should serialize with Option defined") {
    val c = new ContactWithOptionalAddr("Debasish Ghosh",
      Some(Map("primary" -> new Address("10 Market Street", "San Francisco, CA", "94111"),
          "secondary" -> new Address("3300 Tamarac Drive", "Denver, CO", "98301"))))
    c should equal(
      serializer.in[ContactWithOptionalAddr](serializer.out(c)))
  }
}

You can also specify a custom ClassLoader while using sjson serializer:

object SJSON {
  val classLoader = //.. specify a custom classloader
}
import SJSON._
serializer.out(..)
//.. 

Fighting Type Erasure

Because of type erasure, it's not always possible to infer the correct type during de-serialization of objects. Consider the following example:

abstract class A
@BeanInfo case class B(param1: String) extends A
@BeanInfo case class C(param1: String, param2: String) extends A

@BeanInfo case class D(@(JSONTypeHint @field)(value = classOf[A])param1: List[A])

and the serialization code like the following:

object TestSerialize{
 def main(args: Array[String]) {
   val test1 = new D(List(B("hello1")))
   val json = sjson.json.Serializer.SJSON.out(test1)
   val res = sjson.json.Serializer.SJSON.in[D](json)
   val res1: D = res.asInstanceOf[D]
   println(res1)
 }
}

Note that the type hint on class D says A, but the actual instances that have been put into the object before serialization is one of the derived classes (B). During de-serialization, we have no idea of what can be inside D. The serializer.in API will fail since all hint it has is for A, which is abstract. In such cases, we need to handle the de-serialization by using extractors over the underlying data structure that we use for storing JSON objects, which is JsValue. Here's an example:

val test1 = new D(List(B("hello1")))
val json = serializer.out(test1)

// create a JsValue from the string
val js = Js(new String(json))

// extract the named list argument
val m = (Symbol("param1") ? list)
val m(_m) = js

// extract the string within
val s = (Symbol("param1") ? str)

// form a list of B's
val result = _m.map{ e =>
  val s(_s) = e
  B(_s)
}

// form a D
println("result = " + D(result))

The above snippet de-serializes correctly using extractors defined on JsValue. For more details on JsValue and the extractors, please refer to dispatch-json.