Commit Logs

Serialize and deserialize JSON with json4s in Scala

JSON is a widely used protocol for communication between services. Recently, I was tasked to write some serialization/deserialization logic in Scala, so I did a bit research and find json4s to be an effective option for my use case. Here are some simple code snippets to get started with this library.

Basics

JSON AST is used to model the structure of a JSON document, which is the fundamental building block of json4s.

1
2
3
4
5
6
7
8
9
10
11
12
sealed abstract class JValue
case object JNothing extends JValue // 'zero' for JValue
case object JNull extends JValue
case class JString(s: String) extends JValue
case class JDouble(num: Double) extends JValue
case class JDecimal(num: BigDecimal) extends JValue
case class JInt(num: BigInt) extends JValue
case class JBool(value: Boolean) extends JValue
case class JObject(obj: List[JField]) extends JValue
case class JArray(arr: List[JValue]) extends JValue

type JField = (String, JValue)

Features are implemented based on AST such as functions used to transform the AST itself, or between the AST and other formats.

Serialization

We can serialize Scala objects, such as case class into JSON easily with json4s default formats.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.write

implicit val formats = DefaultFormats

case class Employee(id: Int, firstName: String, lastName: String)

case class Address(
streetAddress: String,
city: String,
state: String,
country: String,
zipcode: String
)

case class Company(
id: Int,
name: String,
address: Address,
employees: List[Employee]
)

val piedPier = Company(
id = 1,
name = "pied piper",
address = Address(
streetAddress = "5230 Newell Road",
city = "Palo Alto",
state = "CA",
country = "USA",
zipcode = "94301"
),
employees = List(
Employee(1, "Richard", "Hendricks"),
Employee(2, "Jared", "Dunn"),
Employee(3, "Dinesh", "Chugtal"),
Employee(4, "Bertram", "Gilfoyle"),
Employee(5, "Erlich", "Bachman")
)
)

// serialize piedPier
val piedPierJSON = write(piedPier)

And, walla, you get what you want!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{ "id": 1,
"address": {
"streetAddress": "5230 Newell Road",
"city": "Palo Alto",
"state": "CA",
"country": "USA",
"zipcode": "94301"
},
"employees": [
{
"id": 1,
"firstName": "Richard",
"lastName": "Hendricks"
},
{
"id": 2,
"firstName": "Jared",
"lastName": "Dunn"
},
{
"id": 3,
"firstName": "Dinesh",
"lastName": "Chugtal"
},
{
"id": 4,
"firstName": "Bertram",
"lastName": "Gilfoyle"
},
{
"id": 5,
"firstName": "Erlich",
"lastName": "Bachman"
}
]
}

As you can see, the serialization is super easy for simple case classes and it supports the following out-of-box:

  • Arbitrarily deep case class graphs
  • All primitive types, including BigInt and Symbol
  • List, Seq, Array, Set and Map (note, keys of the Map must be strings: Map[String, _])
  • scala.Option
  • java.util.Date
  • Polymorphic Lists (see below)
  • Recursive types
  • Serialization of fields of a class

Of course, you might already started asking how about more complicated use cases. Luckily, json4s supports customized serialization logic as well with a little bit of extra work.

For illustration, let’s assume our Employee case class is not serializable by default, we can plug in a customized serializer to achieve the same results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class EmployeeSerializer extends CustomSerializer[Employee] (format =>
{
case JObject(
JField("id", JInt(d)) ::
JField("firstName", JString(f)) ::
JField("lastName", JString(l))
) => new Employee(d.int, f.string, l.string)
},
{
case x: Employee => JObject(
JField("id", JInt(Int(x.id))) ::
JField("firstName", JString(String(x.firstName)))) ::
JField("lastName", JString(String(x.lastName)))
}
)

implicit val formats = Serialization.formats(NoTypeHints) + new EmployeeSerializer

The syntax is pretty straightforward to create a customized serializer and you can just plug it in as you would do with the default one.

Deserialization

The deserialization is just a one-liner if you happen to serialize the JSON object with the above logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.read

implicit val formats = DefaultFormats

case class Employee(id: Int, firstName: String, lastName: String)

case class Address(
streetAddress: String,
city: String,
state: String,
country: String,
zipcode: String
)

case class Company(
id: Int,
name: String,
address: Address,
employees: List[Employee]
)

val piedPier = read[Company](piedPierJSON)

In fact, for any JSON string, we just also deserialize it by

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
implicit val formats = DefaultFormats

val piedPierJSON = parse("""
{ "id": 1,
"address": {
"streetAddress": "5230 Newell Road",
"city": "Palo Alto",
"state": "CA",
"country": "USA",
"zipcode": "94301"
},
"employees": [
{
"id": 1,
"firstName": "Richard",
"lastName": "Hendricks"
},
{
"id": 2,
"firstName": "Jared",
"lastName": "Dunn"
},
{
"id": 3,
"firstName": "Dinesh",
"lastName": "Chugtal"
},
{
"id": 4,
"firstName": "Bertram",
"lastName": "Gilfoyle"
},
{
"id": 5,
"firstName": "Erlich",
"lastName": "Bachman"
}
]
}
""")

val piedPier = piedPierJSON.extract[Company]

Summary

I find json4s to be a pleasure to work with because of its simplicity and powerful features. I think it could be a good go-to choice for Scala JSON serialization/deserialization, unless you are not using the Play Framework (Play has its own utilities to work with JSON and I am saving it for a later post).

As always, I would really appreciate your thoughts/comments. Feel free to leave them following this post or tweet me @_LeiG.