However we need to remember that there's a big difference between the richness of type information that a JSON structure has and that which a Scala object can have. Unless you preserve the type information as part of your serialization protocol when going from Scala to JSON, it becomes very tricky and extremely difficult in some cases to do a lossless transformation. And with the JVM, type erasure makes it almost impossible to reconstruct some of the serialized JSON structures into the corresponding original Scala objects.
From ver 0.7, sjson offers JSON serialization protocol that does not use reflection in addition to the original one. 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.
sjson's type class based serialization is inspired from the excellent sbinary by David MacIver (currently maintained by Mark Harrah) and uses the same protocol and even steals many of the implementation artifacts.
For an introduction to the basics of the concepts of type class, its implementation in Scala and how type class based serialization protocols can be designed in Scala, refer to the following blog posts which I wrote a few weeks back:
JSON Serialization of built-in types
Here’s a sample session at the REPL that uses the default serialization protocol of sjson ..
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 type classes 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 a case class in Scala that defines a
Person
abstraction .. But before we look into how this serializes into JSON and back, 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]
Format[]
is the type class that specifies the contract for serialization. For your own abstraction you need to provide an implementation of the Format[]
type class. Let’s do the same for Person
within a specific Scala module. In case you don't remember the role that modules play in type class based design in Scala, they allow selection of the appropriate instance based on the static type checking that the language offers. This is something that you don't get in Haskell.object Protocols {
// person abstraction
case class Person(lastName: String, firstName: String, age: Int)
// protocol definition for person serialization
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 from Nathan Hamblen. 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 type class 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)
}
}
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.