There are a number of reasons I love Lift. I was thinking through some architectural changes that are coming with Anchor Tab soon and I realized that the integration between Lift (the web framework), lift-json (the JSON parsing library), and MongoDB are is pretty sweet. Let’s say that you’re working on a pet-themed website and you need to store information about some cats and dogs. You would probably define something similar to the following data model.
trait Animal { | |
def makeNoise(): String | |
} | |
case class Dog(name: String, breed: String) extends Animal { | |
def makeNoise() = { | |
"Woof" | |
} | |
} | |
case class Cat(name: String, furPattern: String) extends Animal { | |
def makeNoise() = { | |
"Meow" | |
} | |
} | |
case class AnimalInfo(animals: List[Animal]) |
This is an entirely sensible data model. (Albeit, my naming of classes could probably use some work, but this’ll suffice.) So, what would it look like if I wanted to take an instance of one of these objects and turn it into something that could be then turned into JSON? (Let’s say, for an API.) That code would look something like this.
// Import lift-json | |
import net.liftweb.json._ | |
import Extraction._ | |
// Declare a set of serializers and type hints to use. | |
implicit val formats = DefaultFormats | |
// Produce a JValue representation of a dog. This can | |
// be serialized to actual JSON and manipulated with | |
// queries and transforms. | |
val jvalueDog: JValue = decompose(Dog("Shadow", "Collie")) |
In the example above, the jvalueDog is an instance of JValue. You can think of JValues as lift-json’s intermediary format between real domain model objects and actual, stringified JSON. (If we were to be proper, we’d say it’s a part of the Abstract Syntax Tree, but that’s a mouthful.) You’re able to query them for their content, manipulate their structure, and do all varieties of fun things with them – including return them as the result of a function that’s exposed as a REST API, and have Lift handle the serialization processes for you.
Part of what makes lift-json so beautiful is it’s adept use of reflection to automatically figure out what things in a JValue should be named and how things fit together during the deserialization process. But there’s a rub with this reflection and our data model. What if we want to serialize and deserialize an entire AnimalInfo object? We’ll see that we run into a problem. Take, for example, the following code:
// Import lift-json | |
import net.liftweb.json._ | |
import Extraction._ | |
// Declare a set of serializers and type hints to use. | |
implicit val formats = DefaultFormats | |
// Create an animal info instance with a list of animals. | |
val animals = AnimalInfo( | |
Dog("Shadow", "Collie") :: | |
Dog("Beamer", "Collie") :: | |
Cat("Mittens", "Zig-Zag") :: | |
Nil | |
) | |
// Produce a JValue representation of the animals. | |
// Would render to the following JSON: | |
// { animals: [ {name: "Shadow", breed: "Collie"}, ... ] } | |
val jvalueAnimals: JValue = decompose(animals) | |
// Attempt to extract the animals, which will fail with | |
// a no constructor error because Animals is a trait that | |
// can't be directly instantiated. | |
val extractedAnimals = jvalueAnimals.extract[AnimalInfo] |
As noted in the comments, the code above will fail to execute. In fact, not only will it fail, it’ll blow up pretty spectacularly because it can’t directly instantiate an Animal.
scala> val jvalueAnimals: JValue = decompose(animals) | |
jvalueAnimals: net.liftweb.json.package.JValue = JObject(List(JField(animals,JArray(List(JObject(List(JField(name,JString(Shadow)), JField(breed,JString(Collie)))), JObject(List(JField(name,JString(Beamer)), JField(breed,JString(Collie)))), JObject(List(JField(name,JString(Mittens)), JField(furPattern,JString(Zig-Zag))))))))) | |
scala> val extractedAnimals = jvalueAnimals.extract[AnimalInfo] | |
net.liftweb.json.MappingException: No usable value for animals | |
No constructor for type interface Animal, JObject(List(JField(name,JString(Shadow)), JField(breed,JString(Collie)))) | |
at net.liftweb.json.Meta$.fail(Meta.scala:191) | |
at net.liftweb.json.Extraction$.mkValue$1(Extraction.scala:357) | |
at net.liftweb.json.Extraction$.build$1(Extraction.scala:317) | |
at net.liftweb.json.Extraction$$anonfun$12.apply(Extraction.scala:253) | |
at net.liftweb.json.Extraction$$anonfun$12.apply(Extraction.scala:253) | |
at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:233) | |
at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:233) | |
at scala.collection.LinearSeqOptimized$class.foreach(LinearSeqOptimized.scala:59) | |
at scala.collection.immutable.List.foreach(List.scala:76) | |
at scala.collection.TraversableLike$class.map(TraversableLike.scala:233) | |
at scala.collection.immutable.List.map(List.scala:76) | |
at net.liftweb.json.Extraction$.instantiate$1(Extraction.scala:253) |
The solution to this problem lies in a concept in lift-json called type hints. Type hints are attached to the serialization/deserialization chain of lift-json. During serialization, if a class that is being serialized matches one of the classes that we indicate we want a type hint for, lift-json will add a jsonClass parameter to the JValue as it’s being built. To define type hints, all we have to do is adjust our formats definition, and things will start working for us.
// Import lift-json | |
import net.liftweb.json._ | |
import Extraction._ | |
// Declare a set of serializers and type hints to use. | |
implicit val formats = DefaultFormats ++ ShortTypeHints( | |
classOf[Dog] :: | |
classOf[Cat] :: | |
Nil | |
) | |
// Create an animal info instance with a list of animals. | |
val animals = AnimalInfo( | |
Dog("Shadow", "Collie") :: | |
Dog("Beamer", "Collie") :: | |
Cat("Mittens", "Zig-Zag") :: | |
Nil | |
) | |
// Produce a JValue representation of the animals. | |
// Would render to the following JSON: | |
// { animals: [ {jsonClass: "Dog", name: "Shadow", breed: "Collie"}, ... ] } | |
val jvalueAnimals: JValue = decompose(animals) | |
// Attempt to extract the animals, which will succeed because the jsonClass | |
// parameter tells lift-json which actual instance fitting Animal it should | |
// instatiate. | |
val extractedAnimals = jvalueAnimals.extract[AnimalInfo] |
The above example will work because on deserialization, lift-json sees the jsonClass parameter attached to the JValues, sees that they fit the interface Animal, and instantiates them correctly without fuss. This functionality sets us up pretty nicely for seamless polymorphism in the database.
The next step that we’d want to implement in this setup is to mix in MongoDB. Since MongoDB is a document store that (basically) stores JSON objects, lift-json and lift-mongodb end up working pretty well together. Also, unlike a conventional relational database, MongoDB’s non-relational nature means it doesn’t require that each document have the same fields. We can store our cats and dogs side-by-side in our AnimalInfo with no-fuss even though they don’t have the same properties, and without having to drop NULLs in our dataset.
Converting the example code above to a functioning MongoDB representation is pretty simple with lift-mongodb’s MongoDocument and MongoDocumentMeta traits. (We, of course, assume in this code that you’ve already connected to the database elsewhere.)
import net.liftweb.mongodb._ | |
import BsonDSL._ | |
trait Animal { | |
def makeNoise(): String | |
} | |
case class Dog(name: String, breed: String) extends Animal { | |
def makeNoise() = { | |
"Woof" | |
} | |
} | |
case class Cat(name: String, furPattern: String) extends Animal { | |
def makeNoise() = { | |
"Meow" | |
} | |
} | |
// Define AnimalInfo as a mongo model. | |
case class AnimalInfo(animals: List[Animal], _id: ObjectId = ObjectId.get) | |
extends MongoDocument[AnimalInfo] { | |
val meta = AnimalInfo | |
} | |
object AnimalInfo extends MongoDocumentMeta[AnimalInfo] { | |
override def formats = allFormats ++ ShortTypeHints( | |
classOf[Dog] :: | |
classOf[Cat] :: | |
Nil | |
) | |
} | |
// Create and save an animal info. | |
AnimalInfo( | |
Dog("Shadow", "Collie") :: | |
Dog("Beamer", "Collie") :: | |
Cat("Mittens", "Zig-Zag") :: | |
Nil | |
).save | |
//And your data is in Mongo! |
When you go back into your Mongo collection later and pull out that AnimalInfo, you’ll find everything is the correct instance automagically.
Isn’t life grand? This was just something that was on my mind today, so I thought I’d share the joy around.
This was a whirlwind, isolated example of what lift-json and lift-mongodb are capable of, and how little code is required to get something meaningful happening in the Lift/Scala ecosystem. It’s just the tip of the iceberg. If you’re interested in digging into these libraries, or Lift, here are some useful springboards:
As always, leave me some comment love with thoughts, errors discovered, or locations of free cookies. See you shortly. ?