Thursday, November 28, 2013

The quest for a Scala library to manage Json in an Android application

I Search of the perfect library


For one of my project, I needed to produce some Json, store it statically as a text file on a server and retrieve it from my Android client.

Something very usual for a mobile app developer.

In Java world I would probably use Gson library. Then I would give it the Json string, tell hey Gson put that string in an instance of that class. Reflection magic would do the trick and after two lines, the work would be done.

It would look like that (GSon wiki):
class BagOfPrimitives {
  private int value1 = 1;
  private String value2 = "abc";
  private transient int value3 = 3;
  BagOfPrimitives() {}
}
Gson gson = new Gson();
agOfPrimitives obj2 = gson.fromJson(json, BagOfPrimitives.class);

So let's do the same on Scala?

Of course not! Gson is not compliant with case class. Like any other java library by the way. There are probably some trick to apply to make it work, but it would ruin the automagic of such library.

So we need a lib implemented in Scala.

If you search such thing, you would probably find around ten of them.

First you would discover an implementation in the Scala language itself. Unfortunately few seconds after you would notice it s deprecated, the API is ugly and that's not what you want.

So let's find something else.

You will then notice that there is Jerkson, but you need lots of boilerplate code to use it. Moreover it has not been updated since one year... not good.

Built on top of it, Using it for the parsing process (thanks to +Nick Stanchenko for the correction) you will discover a Json lib extracted from the Play framework. Play is a well known product, there are lots of Stack Overflow post about it. FYI, the standalone JSon lib has officially been released, and the dependency to add it to your project is:

"com.typesafe.play" %% "play-json" % "2.2.0"

So I run a test on my project.

My case class is like that:

case class Device(brandName:String, codeName: String, commercialName: String, partitionTable:Map[String, String]) extends Ordered[Device]{

    override def toString = brandName + " (model: " + commercialName + ")\n" + partitionTable.mkString(", ")

    override def compare(that: Device): Int = this.brandName compareTo that.brandName match {
        case 0 => commercialName.compareTo(that.commercialName)
        case compareBrand => compareBrand
      }
  }

And to make it work I just needed to define one implicit Format value. It's very easy to do with the kind of typed static factory. The implicit value will be passed to the fromJson method, providing it the internal structure of the case class.

implicit val jsonDeviceFormat = Json.format[Device]
def getAsyncLayoutTable(s:String) = Json.fromJson[Seq[Device]](Json.parse(s)).asOpt
End of the quest? Of course not!

This library is a standalone version of the Json module from the Play Framework. To work it needs lots of dependencies, about 10. Some from the Play framework itself, some from Jackson/Jerkson lib. As you know, Android application size matters even with Proguard enabled. More important, we are limited to 64K methods per application, after... it's complicated.

So I searched for something else.

And then I found it: Spray.IO

Spray.IO may be less known than the play framework but it's about to be merged with it and to become it's low level layer. So no worries about having long term support of this library and lots of updates.

Two things interesting with the Json module from Spray.IO, it comes with one small dependency only , it's very easy to implement in your project, and more important, it works as advertised :-)

Now let's see how to implement it in your Android project.

II The implementation

First step : add it to the sbt file.

That's very easy, we just need to add the repository and the dependency itself.

Here is the code:
resolvers ++= Seq(
...,
  "spray" at "http://repo.spray.io/"
)

libraryDependencies ++= Seq (
  ...,
  "io.spray" %%  "spray-json" % "1.2.5"
)

Second step: the implementation

The library knows how to manage some type without any work from you (from the README)

Here are the types already taken care of by theDefaultJsonProtocol:
  • Byte, Short, Int, Long, Float, Double, Char, Unit, Boolean 
  • String, Symbol 
  • BigInt, BigDecimal 
  • Option, Either, Tuple1 - Tuple7 
  • List, Array 
  • immutable.{Map, Iterable, Seq, IndexedSeq, LinearSeq, Set, Vector} 
  • collection.{Iterable, Seq, IndexedSeq, LinearSeq, Set} 
  • JsValue

In my my project I need to deserialize a case class. Fortunately my case class is made of types managed by the library natively. So most of the parsing code is already included in the list we have seen.

As many magic thing in Scala we need first to import the library itself in our class, and also import some implicit values (remember, these are the list of type Spray.IO knows how to manage).
import spray.json._
import DefaultJsonProtocol._

Then we need to create a companion object for our case class in order to pimp it. Finally, we will write a small method which will do all the work for us.

Let's write the companion object class extending DefaultJsonProtocol:
object MyJsonProtocol extends DefaultJsonProtocol {
    implicit val colorFormat = jsonFormat4(Device)
}
I use jsonFormat4 because I have 4 arguments in my case class (yeah, it's really complicated :-) ). The argument of the method is my case class itself (code below).

And then you deserialize:
import MyJsonProtocol._
val result = json.get.asJson.convertTo[Seq[Device]]
Don't forget to import your object if your are in a method def!

That's all. Our implicit method will be passed as an implicit argument and the lib will do its work like the Play one.

Voilà! It's done. 

Next time we will see how to get the Json string in an async way with the famous RxJava framework!

Edit: I have not tested it myself, but it seems that Play lib is much more rapid to do the work than the Spray.IO one. The reason is that Play is using the famous Jakson lib and Spray.IO uses Partboiled for the parsing task. Thanks to +Nick Stanchenko for these information!
Post a Comment