Spray: how to (de)serialize objects with json4s

A while ago I wrote an article on how to build a REST api with Spray, where I used the Spray facilities to serialize and deserialize objects to and from JSON.

This article will analyze how we can make the (de)serialization of our case classes easier, by using a library called json4s.

All the code produced in this tutorial can be found here.

Why Json4s?

Although the Spray way of (de)serializing objects works out-of-the-box, it is not always super convenient to use.

In fact, for every entity we need to specify the corresponding JSON format. See for example our QuizProtocol object from the previous tutorial:

// file QuizProtocol.scala
 ...  
  object Quiz extends DefaultJsonProtocol {
    implicit val format = jsonFormat3(Quiz.apply)
  }

  object Question extends DefaultJsonProtocol {
    implicit val format = jsonFormat2(Question.apply)
  }

  object Answer extends DefaultJsonProtocol {
    implicit val format = jsonFormat1(Answer.apply)
  }
...
/* We need to do it for each object
 * and we need to specify the number of fields to serialize
 */

Json4s is an easy-to-use library that automatically (de)serializes case classes into JSON objects: by using this library we will avoid the need of specifying a (de)serialization format for every case class.

Set up

First of all, we need to import the library by adding the dependencies to our build.sbt file:

// build.sbt
libraryDependencies ++= {
  ...
  val Json4sVersion = "3.2.11"

  Seq(
    ...
    "org.json4s" %% "json4s-native" % Json4sVersion,
    "org.json4s" %% "json4s-ext" % Json4sVersion,
    ...
  )
}

We now need to tell json4s about any customization we would like in our serializers.

For example, we can define a different date format to use:

// file JsonSupport.scala
package com.danielasfregola.quiz.management.serializers

import java.text.SimpleDateFormat

import org.json4s.ext.JodaTimeSerializers
import org.json4s.{DefaultFormats, Formats}
import spray.httpx.Json4sSupport

trait JsonSupport extends Json4sSupport {

  implicit def json4sFormats: Formats = customDateFormat ++ JodaTimeSerializers.all

  val customDateFormat = new DefaultFormats {
    override def dateFormatter = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss")
  }
  
}

Also, note that the package json4s-ext offers serializers for types that, although not standard, are quite common. For example, if you use joda.time you will need to add a specific group of serializers, called JodaTimeSerializers.

Json4s Custom Serializers

What happens if you need to serialize a type that is not supported by json4s? Do not dispare! In fact, json4s allows you to specify custom serializers quite easily.

For example, let’s assume that your API uses Timestamp dates and that when, (de)serializing a date in JSON, it should be converted to milliseconds from the unix epoc (i.e.: 1970-01-01 00:00:00 UTC). All you have to do is to create a custom serializer as following:

// file CustomerSerializers.scala
package com.danielasfregola.quiz.management.serializers

import java.sql.Timestamp

import org.json4s.CustomSerializer
import org.json4s.JsonAST.{JInt, JNull}

object CustomSerializers {
  val all = List(CustomTimestampSerializer)
}

case object CustomTimestampSerializer extends CustomSerializer[Timestamp](format =>
  ({
    case JInt(x) => new Timestamp(x.longValue * 1000)
    case JNull => null
  },
    {
      case date: Timestamp => JInt(date.getTime / 1000)
    }))

Don’t forget to add your custom serializers as part of your (de)serialization configuration:

// file JsonSupport.scala
...

trait JsonSupport extends Json4sSupport {

  implicit def json4sFormats: Formats = ... ++ CustomSerializers.all

  ...
}

Usage

Now that the set up is completed we just need to extend the JsonSupport trait every time a JSON format for (de)serialization is required.

For example, our QuitProtocol object now looks a lot simpler than before:

// file QuizProtocol.scala
package com.danielasfregola.quiz.management

import com.danielasfregola.quiz.management.serializers.JsonSupport

object QuizProtocol extends JsonSupport {

  case class Quiz(id: String, question: String, correctAnswer: String)
  
  case object QuizCreated
  
  case object QuizAlreadyExists
  
  case object QuizDeleted
  
  case class Question(id: String, question: String)
  
  case object QuestionNotFound
  
  case class Answer(answer: String)
  
  case object CorrectAnswer
  
  case object WrongAnswer

  implicit def toQuestion(quiz: Quiz): Question = Question(id = quiz.id, question = quiz.question)

  implicit def toAnswer(quiz: Quiz): Answer = Answer(answer = quiz.correctAnswer)
}

Summary

Previously, we have described how to create a REST API using Spray. This article has simplified how to perform (de)serialization in Spray, by using the json4s library.

All the code produced in this tutorial can be found here.