Default and customized lift-json type hints

Not just for in Lift web apps, the lift-json library serves as a useful, fast, and sophisticated method of handling JSON in your Scala projects. In general usage, the lift-json library is pretty smart about serializing/deserializing messages, which are based on case classes. However, under certain circumstances the case classes in question may be indistinguishable, and this is where type hints come into play. Let’s take a look…

Consider that we have case classes for a couple of kinds of animals and want to deal with serializing and deserializing lists of these animals:

trait Animal
case class Cat(name:String, age:Int) extends Animal
case class Dog(name:String, age:Int) extends Animal

val manchester = new Cat("Manchester", 10)
val fido = new Dog("Fido", 7)
val animals = List(manchester, fido)

So we do a couple of imports…

import net.liftweb.json._
import net.liftweb.json.Serialization._

And try writing to JSON and back (pulling in the lift-json DefaultFormats which include defaults for conversion info such as date formatting):

implicit val formats = DefaultFormats
val asJson = write(animals)
println("Marshalled as JSON: " + asJson)
val andBack = read(asJson)
println("Unmarshalled: " + andBack)

Yielding…

Marshalled as JSON: [{"name":"Manchester","age":10},{"name":"Fido","age":7}]
net.liftweb.json.MappingException: No information known about type
	at net.liftweb.json.Extraction$.instantiate$1(Extraction.scala:252)
	at net.liftweb.json.Extraction$.newInstance$1(Extraction.scala:283)
	at net.liftweb.json.Extraction$.build$1(Extraction.scala:301)
	at net.liftweb.json.Extraction$.extract0(Extraction.scala:352)
	at net.liftweb.json.Extraction$.extract(Extraction.scala:42)
	at net.liftweb.json.JsonAST$JValue.extract(JsonAST.scala:300)
	at net.liftweb.json.Serialization$.read(Serialization.scala:48)
	at com.foo.LiftJsonTypeHints$.v1(LiftJsonTypeHints.scala:26)
	at .<init>(<console>:6)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:9)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$scala_repl_result(<console>)
	at sun.reflect.NativeMethodAccessorIm...

Ruh roh.

Ok, we know we have a list of things that are of trait Animal, so let’s make a small change to explicitly read back a List of Animal:

implicit val formats = DefaultFormats
val asJson = write(animals)
println("Marshalled as JSON: " + asJson)
val andBack = read[List[Animal]](asJson)
println("Unmarshalled: " + andBack)

And….hrm….

Marshalled as JSON: [{"name":"Manchester","age":10},{"name":"Fido","age":7}]
net.liftweb.json.MappingException: No constructor for type interface com.foo.Animal, JObject(List(JField(name,JString(Manchester)), JField(age,JInt(10))))
	at net.liftweb.json.Extraction$$anonfun$findBestConstructor$1$1.apply(Extraction.scala:212)
	at net.liftweb.json.Extraction$$anonfun$findBestConstructor$1$1.apply(Extraction.scala:212)
	at scala.Option.getOrElse(Option.scala:104)
	at net.liftweb.json.Extraction$.findBestConstructor$1(Extraction.scala:212)
	at net.liftweb.json.Extraction$.instantiate$1(Extraction.scala:247)
	at net.liftweb.json.Extraction$.newInstance$1(Extraction.scala:283)
	at net.liftweb.json.Extraction$.build$1(Extraction.scala:301)
	at net.liftweb.json.Extraction$$anonfun$17.apply(Extraction.scala:291)
	at net.liftweb.json.Extraction$$anonfun$17.apply(Extraction....

Ok, so we can’t just create a generic animal because Animal has no constructor, and lift-json doesn’t know any better. Let’s introduce type hints for our case classes–note the change in our implicit DefaultFormats to add ShortTypeHints.

implicit val formats = DefaultFormats.withHints(
           ShortTypeHints(List(classOf[Cat], classOf[Dog])))
val asJson = write(animals)
println("Marshalled as JSON: " + asJson)
val andBack = read[List[Animal]](asJson)
println("Unmarshalled: " + andBack)

And…

Animals: List(Cat(Manchester,10), Dog(Fido,7))
Marshalled as JSON: [{"jsonClass":"Cat","name":"Manchester","age":10},{"jsonClass":"Dog","name":"Fido","age":7}]
Unmarshalled: List(Cat(Manchester,10), Dog(Fido,7))

Slick! The JSON-formatted list now includes “jsonClass” attributes to help clue-in lift-json to the type for unmarshalling back to Animal case classes. As an aside, one could use FullTypeHints instead of ShortTypeHints, which will fill in jsonClass as the class name including fully-qualified package structure. So if our classes are defined in package “com.foo”, we’d see “jsonClass”:”com.foo.Cat”.

Ok, let’s take this a step further. I originally was experimenting with this due to a desire to interact with a .NET-based JSON web service, which utilized type hints but with a different attribute name than jsonClass. Luckily, lift-json allows us to specify a custom type hint field name when we define our own Formats instance:

implicit val formats = new Formats {
  val dateFormat = DefaultFormats.lossless.dateFormat
  override val typeHints = 
        ShortTypeHints(List(classOf[Cat], classOf[Dog]))
  override val typeHintFieldName = "type"
}
val asJson = write(animals)
println("Marshalled as JSON: " + asJson)
val andBack = read[List[Animal]](asJson)
println("Unmarshalled: " + andBack)

And here we go!

Marshalled as JSON: [{"type":"Cat","name":"Manchester","age":10},{"type":"Dog","name":"Fido","age":7}]
Unmarshalled: List(Cat(Manchester,10), Dog(Fido,7))

Ok, cool. But this didn’t quite meet my needs, because the API I’m dealing with also uses type hints that start with lowercase letters. To achieve this I can create my own case class extending the TypeHints trait, and then use that instead of ShortTypeHints:

case class DowncasedTypeHints(hints: List[Class[_]]) extends TypeHints {
  def hintFor(msgClass: Class[_]):String = {
    val shortNameIdx = msgClass.getName.lastIndexOf(".") + 1
    msgClass.getName.substring(shortNameIdx,shortNameIdx+1).toLowerCase + 
                   msgClass.getName.substring(shortNameIdx+1)
  }
  def classFor(hint: String) = hints find (hintFor(_) == hint)
}
implicit val formats = new Formats {
  val dateFormat = DefaultFormats.lossless.dateFormat
  override val typeHints = 
         DowncasedTypeHints(List(classOf[Cat], classOf[Dog]))
  override val typeHintFieldName = "type"
}
val asJson = write(animals)
println("Marshalled as JSON: " + asJson)
val andBack = read[List[Animal]](asJson)
println("Unmarshalled: " + andBack)

And as expected our output has typehints made up of the class names lowercased:

Animals: List(Cat(Manchester,10), Dog(Fido,7))
Marshalled as JSON: [{"type":"cat","name":"Manchester","age":10},{"type":"dog","name":"Fido","age":7}]
Unmarshalled: List(Cat(Manchester,10), Dog(Fido,7))

(You can clean up this last example even more using implicit conversions!)

, , , ,

4 Comments

  • Colin says:

    You are a lifesaver. I’ve spent all morning trying to work out a very similar scenario – I have a type parameter from the Yammer API.

    Haven’t tried it out yet but what you have looks spot on for my needs. Thank you again for taking the time to document this so clearly :-)

  • Colin says:

    Well, I’m nearly there. I get the following exception:

    [MappingException: No usable value for type Did not find value which can be converted into java.lang.String]

    It seems to be caused by the “override val typeHintFieldName” line. I don’t suppose you have any ideas?

  • Colin says:

    Ha! Worked that last one out – I had a case class with an attribute called `type`.

    Thanks again!

  • jamie says:

    Glad you’re squared away, Colin. You’re welcome and happy coding.

Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>