Skip to content

Typeclass based JSON Serialization

debasishg edited this page Feb 15, 2011 · 1 revision

For an introduction to the basics of the concepts of typeclass, its implementation in Scala and how typeclass based serialization protocols can be designed in Scala, refer to the following blog posts:

From ver 0.7, sjson offers JSON serialization protocol that does not use reflection. This is useful in the sense that the user gets to define his own protocol for serializing custom objects to JSON. Whatever you did with annotations in Reflection based JSON Serialization, you can define custom protocol to implement them. Here's an example ..

JSON Serialization of built-in types

Here's a sample session at the REPL ..

scala> import sjson.json._
import sjson.json._

scala> import DefaultProtocol._
import DefaultProtocol._

scala> val str = "debasish"
str: java.lang.String = debasish

scala> import JsonSerialization._
import JsonSerialization._

scala> tojson(str)
res0: dispatch.json.JsValue = "debasish"

scala> fromjson[String](res0)
res1: String = debasish

Now consider a generic data type List in Scala. Here's how the protocol works ..

scala> val list = List(10, 12, 14, 18)
list: List[Int] = List(10, 12, 14, 18)

scala> tojson(list)
res2: dispatch.json.JsValue = [10, 12, 14, 18]

scala> fromjson[List[Int]](res2)
res3: List[Int] = List(10, 12, 14, 18)

Define your Class and Custom Protocol

In the last section we saw how default protocols based on typeclasses are being used for serialization of standard data types. If you have your own class, you can define your custom protocol for JSON serialization.

Consider the following case class in Scala that defines a Person abstraction ..

case class Person(lastName: String, firstName: String, age: Int)

Here's the generic serialization protocol in sjson :-

trait Writes[T] {
  def writes(o: T): JsValue
}

trait Reads[T] {
  def reads(json: JsValue): T
}

trait Format[T] extends Writes[T] with Reads[T]

As part of implementing the protocol, you need to define the Format for the specific abstraction. Let's do the same for Person.

object Protocols {
  case class Person(lastName: String, firstName: String, age: Int)
  object PersonProtocol extends DefaultProtocol {
    import dispatch.json._
    import JsonSerialization._

    implicit object PersonFormat extends Format[Person] {
      def reads(json: JsValue): Person = json match {
        case JsObject(m) =>
          Person(fromjson[String](m(JsString("lastName"))), 
            fromjson[String](m(JsString("firstName"))), fromjson[Int](m(JsString("age"))))
        case _ => throw new RuntimeException("JsObject expected")
      }

      def writes(p: Person): JsValue =
        JsObject(List(
          (tojson("lastName").asInstanceOf[JsString], tojson(p.lastName)), 
          (tojson("firstName").asInstanceOf[JsString], tojson(p.firstName)), 
          (tojson("age").asInstanceOf[JsString], tojson(p.age)) ))
    }
  }
}

Note that the implementation of the protocol uses the dispatch-json library for json. Basically the methods writes and reads define how the JSON serialization will be done for my Person object. Now we can fire up a scala REPL and see it in action :-

scala> import sjson.json._
import sjson.json._

scala> import Protocols._
import Protocols._

scala> import PersonProtocol._
import PersonProtocol._

scala> val p = Person("ghosh", "debasish", 20)
p: sjson.json.Protocols.Person = Person(ghosh,debasish,20)

scala> import JsonSerialization._
import JsonSerialization._

scala> tojson[Person](p)         
res1: dispatch.json.JsValue = {"lastName" : "ghosh", "firstName" : "debasish", "age" : 20}

scala> fromjson[Person](res1)
res2: sjson.json.Protocols.Person = Person(ghosh,debasish,20)

We get serialization of the object into JSON structure and then back to the object itself. The methods tojson and fromjson are part of the Scala module that uses the typeclass Format as implicits. Here's how we define it ..

object JsonSerialization {
  def tojson[T](o: T)(implicit tjs: Writes[T]): JsValue = {
    tjs.writes(o)
  }

  def fromjson[T](json: JsValue)(implicit fjs: Reads[T]): T = {
    fjs.reads(json)
  }
}

h3. Verbose ?

Sure .. you have to do a lot of stuff to define the protocol for your class. If you have a case class, the sjson has some out of the box magic for you where you can do away with all the verbosity. Once again the Scala's type system to the rescue.

Let's see how the protocol can be extended for your custom classes using a much less verbose API which applies only for case classes. Here's a session at the REPL ..

scala> case class Shop(store: String, item: String, price: Int)
defined class Shop

scala> object ShopProtocol extends DefaultProtocol {
     |   implicit val ShopFormat: Format[Shop] = 
     |       asProduct3("store", "item", "price")(Shop)(Shop.unapply(_).get)
     |   }
defined module ShopProtocol

scala> import ShopProtocol._
import ShopProtocol._

scala> val shop = Shop("Shoppers Stop", "dress material", 1000)
shop: Shop = Shop(Shoppers Stop,dress material,1000)

scala> import JsonSerialization._
import JsonSerialization._

scala> tojson(shop)
res4: dispatch.json.JsValue = {"store" : "Shoppers Stop", "item" : "dress material", "price" : 1000}

scala> fromjson[Shop](res4)
res5: Shop = Shop(Shoppers Stop,dress material,1000)

If you are curious about what goes on behind the asProduct3 method, feel free to peek into the source code.

For more examples, have a look at Examples of Type Class based JSON Serialization