Easy JSON (un)marshalling in Scala with Jackson
There's a nice add-on module for Jackson to support Scala data types. Here's the dependencies:
libraryDependencies += "com.fasterxml.jackson.core" % "jackson-databind" % "2.2.2",
libraryDependencies += "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.2.2"
I've written a little convenience wrapper around it to demonstrate its use
import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper}
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
object JsonUtil {
val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
def toJson(value: Map[Symbol, Any]): String = {
toJson(value map { case (k,v) => k.name -> v})
}
def toJson(value: Any): String = {
mapper.writeValueAsString(value)
}
def toMap[V](json:String)(implicit m: Manifest[V]) = fromJson[Map[String,V]](json)
def fromJson[T](json: String)(implicit m : Manifest[T]): T = {
mapper.readValue[T](json)
}
}
Now you can easily (un)marshall any valid JSON structure:
/*
* (Un)marshalling a simple map
*/
val originalMap = Map("a" -> List(1,2), "b" -> List(3,4,5), "c" -> List())
val json = JsonUtil.toJson(originalMap)
// json: String = {"a":[1,2],"b":[3,4,5],"c":[]}
val map = JsonUtil.toMap[Seq[Int]](json)
// map: Map[String,Seq[Int]] = Map(a -> List(1, 2), b -> List(3, 4, 5), c -> List())
/*
* Unmarshalling to a specific type of Map
*/
val mutableSymbolMap = JsonUtil.fromJson[collection.mutable.Map[Symbol,Seq[Int]]](json)
// mutableSymbolMap: scala.collection.mutable.Map[Symbol,Seq[Int]] = Map('b -> List(3, 4, 5), 'a -> List(1, 2), 'c -> List())
/*
* (Un)marshalling nested case classes
*/
case class Person(name: String, age: Int)
case class Group(name: String, persons: Seq[Person], leader: Person)
val jeroen = Person("Jeroen", 26)
val martin = Person("Martin", 54)
val originalGroup = Group("Scala ppl", Seq(jeroen,martin), martin)
// originalGroup: Group = Group(Scala ppl,List(Person(Jeroen,26), Person(Martin,54)),Person(Martin,54))
val groupJson = JsonUtil.toJson(originalGroup)
// groupJson: String = {"name":"Scala ppl","persons":[{"name":"Jeroen","age":26},{"name":"Martin","age":54}],"leader":{"name":"Martin","age":54}}
val group = JsonUtil.fromJson[Group](groupJson)
// group: Group = Group(Scala ppl,List(Person(Jeroen,26), Person(Martin,54)),Person(Martin,54))
If you really want to have fun, you can even monkey patch Scala classes to simplify (un)marshalling
object MarshallableImplicits {
implicit class Unmarshallable(unMarshallMe: String) {
def toMap: Map[String,Any] = JsonUtil.toMap(unMarshallMe)
def toMapOf[V]()(implicit m: Manifest[V]): Map[String,V] = JsonUtil.toMapOf[V](unMarshallMe)
def fromJson[T]()(implicit m: Manifest[T]): T = JsonUtil.fromJson[T](unMarshallMe)
}
implicit class Marshallable[T](marshallMe: T) {
def toJson: String = JsonUtil.toJson(marshallMe)
}
}
If you import the implicit conversions you can call toJson on any object and toMap or fromJson on any String.
import utils.MarshallableImplicits._
case class Person(name:String, age: Int)
val jeroen = Person("Jeroen", 26)
val jeroenJson = jeroen.toJson
// jeroenJson: String = {"name":"Jeroen","age":26}
val jeroenMap = jeroenJson.toMap
// jeroenMap: Map[String,Any] = Map(name -> Jeroen, age -> 26)
Happy coding :)
Written by Jeroen Rosenberg
Related protips
8 Responses
Thanks. Finally figured this out after breaking my head over numerous Scala json libraries that are not as performant or reliable as Jackson. Would have saved me a day or two of trying them all out (especially play json and json4s) if had found this earlier :-)
Thanks for the tutorial! One question: Is mapper.readValue[T](json)
thread safe?
I tried described approach, but it fails in my case.
Having Scala 2.10.4
and Jackson libs 2.2.2
the readValue
method fails on
com.fasterxml.jackson.databind.JsonMappingException: Argument #0 of constructor [constructor for A$A20$A$A20$Group, annotations: [null]] has no property name annotation; must have name when multiple-paramater constructor annotated as Creator
More details here https://gist.github.com/anonymous/7dc05bfdefe7a9c68d9a. Right now I am trying to find out the correct form of annotations.
Update - the issue described seems to be related purely to scala worksheets (I tend to use them for playing and prototyping).
Is there a way to do this without the: ... .experimental.ScalaObjectMapper
?
Tried using ObjectMapper
, based on this post, but ran into complications with: mapper.readValue[T](json)
:(
Had to simplify, but got it to work this way:
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import scala.reflect.ClassTag
import scala.reflect._
object JsonUtil {
val jacksonMapper = new ObjectMapper()
jacksonMapper.registerModule(DefaultScalaModule)
//mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
def toJson(value: Map[Symbol, Any]): String = {
toJson(value map { case (k,v) => k.name -> v})
}
def toJson(value: Any): String = {
jacksonMapper.writeValueAsString(value)
}
def fromJson[T: ClassTag](json: String): T = {
jacksonMapper.readValue[T](json, classTag[T].runtimeClass.asInstanceOf[Class[T]])
}
}
And then can parse to a map using:
JsonUtil.fromJson[Map[String, List[Map[String, String]]]](jsonStr1)
for: //{"key": "value"}
JsonUtil.fromJson[Map[String, List[Map[String, String]]]](jsonStr2)
for:
//{"key": [
"subKey": "val"
...
]}
val jeroenMap = jeroenJson.toMap
is not compiled anyway. While the compiler complain
Note that implicit conversions are not applicable because they are ambiguous: both method augmentString in object Predef of type (x: String)scala.collection.immutable.StringOps and method Unmarshallable in object MarshallableImplicits of type (unMarshallMe: String)utils.MarshallableImplicits.Unmarshallable are possible conversion functions from JsonTest.jeroenJson.type to ?{def toMap: ?}
Any workaround or suggestion are appreciate.
In response to @chandsir, Ì had to change:
def toMap: Map[String,Any] = JsonUtil.toMap(unMarshallMe)
to
def toMapAny: Map[String,Any] = JsonUtil.toMap[Any](unMarshallMe)
and
val jeroenMap = jeroenJson.toMap
to
val jeroenMap = jeroenJson.toMapAny
And it worked.