Json4s Examples: Common Basic Operations using Jackson as Backend
Last updated:- Installing
- Imports
- Parsing a json string into a JValue or JObject
- Extracting a field from a JObject
- Build a JObject step by step
- Parse a json string into a case class
- Creating and iterating over the elements of a JArray
- Transform models to and from json strings using read and write
- Custom serializer
- Json4s DSL
I've previously used the Play 2 Json library and I was reasonably satisfied with it but I was asked to start using json4s since it's bundled by default in Akka, Spray and Spark and we would rather not pull in any extra dependencies right now.
For actual running code see this json4s sandbox project
So here are a couple of examples of some basic use cases to get you started with json4s, using Jackson as the backend.
Installing
In build.sbt
:
libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.3.0"
Imports
import org.json4s._
import org.json4s.jackson.JsonMethods._
Parsing a json string into a JValue or JObject
parse("""{"foo":"bar"}""")
// JValue
parse("""{"foo":"bar"}""").asInstanceOf[JObject]
// JObject
Extracting a field from a JObject
Existing attributes return values of types such as JInt
, JString
and so on. Nonexisting values return JNothing
. You can call toOption
to have it return Option[JValue]
for all cases.
val myObj = parse("""{"foo":"bar"}""").asInstanceOf[JObject]
myObj \ "foo"
// JString
(myObj \ "foo").toOption
// Some[JString]
myObj \ "baz"
// JNothing
(myObj \ "baz").toOption
// None
Build a JObject step by step
A Json object is a sequence of key value pairs, where the key is a String
and the value is a JValue
. You can use either tuple or arrow notation to build it.
JObject("id", JInt(100))
// { "id": 100 }
// you can also use "->"
JObject("id" -> JInt(100))
// { "id": 100 }
Parse a json string into a case class
// json4s requires you to have this in scope to call extract
implicit val formats = DefaultFormats
case class Person(name:String, age: Int)
val jsValue = parse("""{"name":"john", "age": 28}""")
val p = jsValue.extract[Person]
// p is Person("john",28)
val maybeP = jsValue.extractOpt[Person]
// safer version, maybeP is of type Option[Person]
Option
attributes are also supported:
// car may or may not have an owner
case class Car(model:String,year:Int,ownerName:Option[String])
val car1 = parse("""{"model":"c-class","year":2011}""").extract[Car]
// car1: Car = Car(c-class,2011,None)
val car2 = parse("""{ "model":"b-class","year":2013,"ownerName": "john doe"}""").extract[Car]
// car2: Car = Car(b-class,2013,Some(john doe))
Creating and iterating over the elements of a JArray
JSON arrays may have elements of different types, such as strings or ints
val jarr = JArray()
// empty JArray, or []
val jarr2 = JArray(List(JString("foo"),JInt(42)))
// equivalent to ["foo",42]
You can all .arr
on any JArray to access its underlying Scala collection:
val jsvalues = jarr2.arr
// it's a List[JsValue]
jarr2.arr.foreach{ jsval =>
println(s"Hi i'm a ${jsval.getClass}")
}
// prints
// Hi i'm a class org.json4s.JString
// Hi i'm a class org.json4s.JInt
Transform models to and from json strings using read
and write
Chances are the most common operations you may want to do are converting models (e.g. case classes) to Json strings and vice versa.
To convert a case class to a json object:
// json4s requires you to have this in scope to call write/read
implicit val formats = DefaultFormats
case class Person(name:String,age:Int)
val john = Person("john",45)
println(write(john))
// prints """{"name":"john","age":45}"""
Reading a json string into a matching model is also easy, just use read
val maryAsString = """{"name":"mary", "age":89} """
println(read[Person](maryAsString))
// prints Person(mary,89)
It may, of course, be the case that the json string cannot be read into the model at all:
val invalidPerson = """{"name":"david","numPets":2}"""
read[Person](invalidPerson)
// [error] org.json4s.package$MappingException: No usable value for age
// [error] Did not find value which can be converted into int
// org.json4s.package$MappingException: No usable value for age
Json4s is also smart enough to deal with optional (Option[]
) fields; you can write models that contain optional fields and it will behave as you expect:
import org.json4s.jackson.Serialization.write
implicit val formats = DefaultFormats
case class Person(name:String, age: Option[Int])
val john = Person("john",None)
write(john)
// {"name":"john"}
val mary = Person("mary",Some(20))
write(mary)
//{"name":"mary","age":20}
You can also read json strings into models containing optional models:
import org.json4s.jackson.Serialization.read
implicit val formats = DefaultFormats
case class Person(name:String, age: Option[Int])
val john = read[Person]("""{"name":"john"}""")
// Person(john,None)
val mary = read[Person]("""{"name":"mary","age":20}""")
// Person(mary,Some(20))
Custom serializer
For more info on Java 8 java.time APIs, see some general examples and timezone examples
The previous point works well if you're not using anything other than primitive types and case classes.
If you want to use custom types you will have to define a custom serializer so that Json4s knows how to serialize/deserialize it.
For example, if you use the new java.time
API, you'll not be able to read/write it:
import org.json4s._
import org.json4s.jackson.Serialization.{read,write}
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
implicit val formats = DefaultFormats
case class Person(name: String, lastLogin: ZonedDateTime)
val peter = Person("peter",ZonedDateTime.now())
// json4s fails to write the datetime object so it defaults to
// an empty object
println(write(peter))
// prints {"name":"peter","lastLogin":{}}
But if you define a custom serializer for the given type (ZonedDateTime
) and append it to DefaultFormats
, everything works:
import org.json4s._
import org.json4s.jackson.Serialization.{read,write}
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
// a custom serializer has two partial functions, one
// for serializing and one for deserializing
case object ZDTSerializer extends CustomSerializer[ZonedDateTime](format => ( {
case JString(s) => ZonedDateTime.parse(s)
}, {
case zdt: ZonedDateTime => JString(zdt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX")))
}))
// this is important: you can add custom serializers to the
// default serializers, provided by DefaultFormats
implicit val formats = DefaultFormats + ZDTSerializer
case class Person(name: String, lastLogin: ZonedDateTime)
val peter = Person("peter",ZonedDateTime.now())
println(write(peter))
// prints {"name":"peter","lastLogin":"2016-05-04T01:14:46-03:00"}
Json4s DSL
The Json4s folks have also put together a DSL to make it easier to build json. You just need to import org.json4s.JsonDSL._
to have String
s implicitly converted to JString
s, ints to JInt
s and it also deals with sequences and optional values.
It makes it somewhat less cumbersome to create structured, compiled json:
import org.json4s.JsonDSL._
import org.json4s._
// connect tuples with "~" to create a json object
val obj1: JObject = ("foo", "bar") ~ ("baz", "quux")
// {"foo":"bar","baz":"quux"}
// you can also use "->" notation to create tuples
val obj2: JObject = ("foo" -> "bar") ~ ("baz" -> "quux")
println(write(obj2))
// {"foo":"bar","baz":"quux"}
// use any sequence to make an array of json elements
val array1: JArray = Seq(obj1,obj2)
println(write(array1))
// [{"foo":"bar","baz":"quux"},{"foo":"bar","baz":"quux"}]
val array2: JArray = Seq("foo","bar")
println(write(array2))
// ["foo","bar"]