QuickKnol: Lift Json, ShortTypeHints and Scala Traits


We use the awesome Lift Json library in quite a few our projects. It allows for easy customization and is handy for writing and parsing JSON strings. One of the situations which could possibly push you against the wall is when you have classes extending traits. Let us look at this quick example

trait Reducer 

case class SentimentReducer(name:String, coefficientLevel:Double) extends Reducer

case class EmotionReducer(name:String, coefficientLevel:Double) extends Reducer

case class Analysis(dataCubeName:String, reductionSteps:List[Reducer])

We have the situation like above where we have the Reducer trait. The Reducer can have multiple implementations as we have above. Finally, the Analysis class takes the data cube on which it has to work and a list of reductionSteps each one of which is individually a Reducer.

Let us assume that this class comes to us as a JSON string over a REST service. Let us write a test case for JSON write first.

test("JSON serialization of Analysis data") {
    val analysis = new Analysis("TwitterAnalysis", List(new EmotionReducer("Emo1", 0.1), new   SentimentReducer("Sen1", 0.5)))

    val jsonAnalysis = write(analysis)
    assert(jsonAnalysis === """{"dataCubeName":"TwitterAnalysis","reductionSteps":[{"name":"Emo1","coefficientLevel":0.1},{"name":"Sen1","coefficientLevel":0.5}]}""")
  }

Ok, this one works well. Now, let us try to do the reverse wherein, we get a JSON string and we would try to convert it to an Analysis object.

  test("Parsing of Analysis JSON String into Analysis class"){
    val jsonAnalysis = """{"dataCubeName":"TwitterAnalysis","reductionSteps":[{"name":"Emo1","coefficientLevel":0.1},{"name":"Sen1","coefficientLevel":0.5}]}"""
    val analysis = parse(jsonAnalysis).extract[Analysis]
    assert(analysis.reductionSteps(0).isInstanceOf[EmotionReducer])
  }

Interestingly, this blows up with the following error

net.liftweb.json.MappingException: No usable value for reductionSteps
No constructor for type interface com.knoldus.knol.Reducer, JObject(List(JField(name,JString(Emo1)), JField(coefficientLevel,JDouble(0.1))))
  at net.liftweb.json.Meta$.fail(Meta.scala:191)
  at net.liftweb.json.Extraction$.mkValue$1(Extraction.scala:357)
  at net.liftweb.json.Extraction$.net$liftweb$json$Extraction$$build$1(Extraction.scala:317)
  at net.liftweb.json.Extraction$$anonfun$13.apply(Extraction.scala:253)
  at net.liftweb.json.Extraction$$anonfun$13.apply(Extraction.scala:253)
  at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
  at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:244)
  at scala.collection.immutable.List.foreach(List.scala:318)
  at scala.collection.TraversableLike$class.map(TraversableLike.scala:244)
  at scala.collection.AbstractTraversable.map(Traversable.scala:105)

Here, LiftJSON is not able to identify how it can convert the Reducers to their respective classes as there is no constructor which it can use directly.

Now, is the time to give some hints to the Lift formats. We add the following as hints

trait LiftJsonHelper {
  implicit val formats = new DefaultFormats { outer =>
    override val typeHintFieldName = "type"
    override val typeHints = ShortTypeHints(List(classOf[SentimentReducer], classOf[EmotionReducer]))
  }
}

Once this is done, let us see what kind of JSON string is generated. As you would notice that now even at the time of creating the JSON string, lift json would add the type hints along

{“dataCubeName”:”TwitterAnalysis”,”reductionSteps”:[{“type”:”EmotionReducer”,”name”:”Emo1″,”coefficientLevel”:0.1},{“type”:”SentimentReducer”,”name”:”Sen1″,”coefficientLevel”:0.5}]}

Now, if we try to re-run the test scenario with the modified string, we would get the object back in the right structure.

test("Parsing of Analysis JSON String into Analysis class") {
    val jsonAnalysis = """{"dataCubeName":"TwitterAnalysis","reductionSteps":[{"type":"EmotionReducer","name":"Emo1","coefficientLevel":0.1},{"type":"SentimentReducer","name":"Sen1","coefficientLevel":0.5}]}"""
    val analysis = parse(jsonAnalysis).extract[Analysis]
    assert(analysis.reductionSteps(0).isInstanceOf[EmotionReducer])
  }

If you notice, we have overridden typeHintFieldName. By default, it is jsonClass which isn’t very intuitive so we replaced it with type.

override val typeHintFieldName = “type”

You can find the sample project with the above changes on the Knoldus GitHub account.

Advertisements

About Vikas Hazrati

Vikas is the Founding Partner @ Knoldus which is a group of software industry veterans who have joined hands to add value to the art of software development. Knoldus does niche Reactive and Big Data product development on Scala, Spark and Functional Java. Knoldus has a strong focus on software craftsmanship which ensures high-quality software development. It partners with the best in the industry like Lightbend (Scala Ecosystem), Databricks (Spark Ecosystem), Confluent (Kafka) and Datastax (Cassandra). To know more, send a mail to hello@knoldus.com or visit www.knoldus.com
This entry was posted in LiftWeb, Scala. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s