Json parsing, Scala way



Most java developers are familiar with json parsing and object mapping using Jackson library's object mapper functionality that enables serializing POJOs to json string and back. In scala, using the play's json inception mechanism provides a subtle way to serialize json. Using the powerful Scala macros, (a macro is a piece of Scala code, executed at compile-time, which manipulates and modifies the AST of a Scala compile-time metaprogramming), it is able to introspect code at compile-time based on Scala reflection API, access all imports, implicits in the current compile context and generate code. This means the case classes are automatically serialized to json. Also, you can explicitly provide the path to json key and map the value to object's field. But, for simple case classes they are just another boiler-plate code. Use it when we need more powerful mapping and logic for serialized fields. So how does this mapping works? The compiler will inject code into compiled scala AST (Absract Syntax Tree) as the macro-compiler replaces, say, Json.reads[T] by injecting into compile chain and eventually writes out the code for mapping fields in json to object. Internally, play's json module use Jackson's object mapper (ref: play.api.libs.json.jackson.JacksonJson). 

You can add dependency in build.sbt in a minimal-scala project which will provide Json APIs from play framework -
  "com.typesafe.play" %% "play-ws" % "2.4.2" withSources()

For eg, if we have to two classes (in this case class),

case class Region(name: String, state: Option[String])
case class Sales(count: Int, region: Region)

You have to add the implicit  methods for reading and writing to and from json and objects. The methods marked implicit will be inserted for you by the compiler and type is inferred from the context. Any compilation will fail if no implicit value of the right type is available in scope.

implicit val readRegion = Json.reads[Region]
implicit val readSales = Json.reads[Sales]
implicit val writeRegion = Json.writes[Region]
implicit val writeSales = Json.writes[Sales]

If you interchange the order, from readRegion and readSales, you will get compilation error.As the compiler creates a Reads[T] by resolving case class fields & required implicits at COMPILE-time, If any missing implicit is discovered, compiler will break with corresponding error.

 Error:(12, 38) No implicit format for test.Region available.
   implicit val readSales = Json.reads[Sales]
 

Interesting method to try is the validate() method while converting json to object which will help to pin point the path of error.

Executing the following program:



Results:

This is testing json..
Test 1
-------
Result:Some(Sales(123,Region(West,None)))
Test 2
-------
Error at JsPath: /region/name
error.path.missing
()
Result:None
Test 3
------
Error at JsPath: /count
error.expected.jsnumber
Error at JsPath: /region/name
error.expected.jsstring
()
Result:None
Test 4
------
Result:{"count":123,"region":{"name":"West","state":"California"}}
Process finished with exit code 0



                   



1 comment:

  1. Wonderful post.
    If you need to deep in more advanced stuff for play-json give a look to my library https://github.com/aparo/play-json-extra that provided extra stuff such as support of macro annotion for json serialization, variants, autodefault fields in missing JSON.

    ReplyDelete