-
Notifications
You must be signed in to change notification settings - Fork 31
Applicative style APIs for JSON serialization
[Moved to sjson-scalaz]
For an introduction to the basics of the concepts of applicative based API design, its implementation in Scala and how more functional serialization protocols can be designed in Scala, refer to the following blog post:
The implementation in sjson has dependency on scalaz, the Haskell like purely functional library for Scala.
From ver 0.9, sjson offers JSON serialization protocol that offers more composability to users. fromjson
now returns a Validation[_,_]
which can be used to accumulate all errors encountered during de-serialization. Other versions of sjson APIs use exceptions to handle such error cases. And, exceptions, being side-effects, don't compose.
Here's an example of a successful de-serialization, assuming we have the proper typeclasses for Address
and Person
defined within the scope ..
val a = Address(12, "Monroe Street", "Denver", "80231")
val p = Person("ghosh", "debasish", 20, a)
fromjson[Person](tojson(p)) should equal(p.success)
Now consider the following case, where we have the Address
class defined as :
case class Address(no: Int, street: String, city: String, zip: String)
object IncorrectAddressProtocol extends DefaultProtocol {
implicit object AddressFormat extends Format[Address] {
def reads(json: JsValue): ValidationNEL[String, Address] = json match {
case m@JsObject(_) =>
(field[Int]("number", m) |@| field[String]("stret", m) |@| field[String]("City", m) |@| field[String]("zip", m)) { Address }
case _ => "JsObject expected".fail.liftFailNel
}
def writes(p: Address): JsValue =
//.. same as before
}
}
Note that the de-serialization API uses incorrect key names to get the values from the JSON structure.
In the previous versions of the typeclass based API, we would get an exception for the first incorrect key. Now with applicatives being used as the return type of fromjson
, we can do like the following ..
val a = Address(12, "Monroe Street", "Denver", "80231")
(fromjson[Address](tojson(a))).fail.toOption.get.list should equal (List("field number not found", "field stret not found", "field City not found"))
fromjson
returns a Validation, which can be used as an Applicative in scalaz. For more details, have a look at the scalaz codebase.
Hence the errors can be accumulated and reported all at once.
h2. More Composability
A layer of monads on top of your API makes your API composable with any other monad in the world. With sjson de-serialization returning a Validation, we can get better composability when writing complex serialization code like the following. Consider this JSON string from where we need to pick up fields selectively and make a Scala object ..
val jsonString =
"""{
"lastName" : "ghosh",
"firstName" : "debasish",
"age" : 40,
"address" : { "no" : 12, "street" : "Tamarac Square", "city" : "Denver", "zip" : "80231" },
"phone" : { "no" : "3032144567", "ext" : 212 },
"office" :
{
"name" : "anshinsoft",
"address" : { "no" : 23, "street" : "Hampden Avenue", "city" : "Denver", "zip" : "80245" }
}
}"""
We would like to cherry pick a few of the fields from here and create an instance of Contact
class ..
case class Contact(lastName: String, firstName: String,
address: Address, officeCity: String, officeAddress: Address)
Try this with the usual approach as shown above and you will find some of the boilerplate repetitions within your implementation ..
import dispatch.json._
import Js._
val js = Js(jsonString) // js is a JsValue
(field[String]("lastName", js) |@|
field[String]("firstName", js) |@|
field[Address]("address", js) |@|
field[String]("city", (('office ! obj) andThen ('address ? obj))(js)) |@|
field[Address]((('office ! obj) andThen ('address ! obj)), js)) { Contact } should equal(c.success)
Have a look at this how we need to repeatedly pass around js
, though we never modify it any time. Since our field
API is monadic, we can compose all invocations of field together with a Reader monad.
But for that we need to make a small change in our field
API. We need to make it curried .. Here are 2 variants of the curried field
API ..
// curried version: for lookup of a String name
def field_c[T](name: String)(implicit fjs: Reads[T]) = { js: JsValue =>
val JsObject(m) = js
m.get(JsString(name)).map(fromjson[T](_)(fjs)).getOrElse(("field " + name + " not found").fail.liftFailNel)
}
// curried version: we need to get a complete JSON object out
def field_c[T](f: (JsValue => JsValue))(implicit fjs: Reads[T]) = { js: JsValue =>
try {
fromjson[T](f(js))(fjs)
} catch {
case e: Exception => e.getMessage.fail.liftFailNel
}
}
Note how in the second variant of field_c
, we use the extractors of dispatch-json to take out nested objects from a JsValue
structure. We use it below to get the office address from within the parsed JSON.
And here's how we compose all lookups monadically and finally come up with the Contact
instance ..
// reader monad
val contact =
for {
last <- field_c[String]("lastName")
first <- field_c[String]("firstName")
address <- field_c[Address]("address")
office <- field_c[Address]((('office ! obj) andThen ('address ! obj)))
}
yield(last |@| first |@| address |@| office)
// city needs to be parsed separately since we are working on part of js
val city = field_c[String]("city")
// compose everything and build a Contact
(contact(js) |@| city((('office ! obj) andThen ('address ? obj))(js))) {
(last, first, address, office, city) =>
Contact(last, first, address, city, office) } should equal(c.success)