Json4s Examples: Common Basic Operations using Jackson as Backend

Json4s Examples: Common Basic Operations using Jackson as Backend

Last updated:
Json4s Examples: Common Basic Operations using Jackson as Backend
Source
Table of Contents

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 Strings implicitly converted to JStrings, ints to JInts 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"]

See also

Dialogue & Discussion