Spray-json in Scala for Case Class Serialization

Jamil Najafov
Insider Engineering
4 min readJul 18, 2022

Spray-json JSON Serialization

Spray-json is a Scala JSON library with official integration support by Akka HTTP. It requires explicitly defined formats for each type to perform serialization. The application of these formats for generic case classes and classes with type parameters will be explored below.

A JSON format should be available when serialization is performed. A variable or a function with type JsonFormat[T] or RootJsonFormat[T] should be in the implicit context. It can be imported, or defined within a class or a method where parsing or converting to JSON occurs.

JsonFormat is a composite trait that defines a JsonReader and a JsonWriter. JsonReader trait defines a single function read that converts a JsValue which is parsed from String to the given type T. JsonWriter trait defines a single function write that converts the given type T to JsValue which in turn is compiled to a String.

This configuration gives flexibility in object serialization/deserialization. However, most use cases are covered need only one-to-one serialization/deserialization, hence using predefined primitive type formats from DefaultJsonProtocol and format generators for product types (jsonFormat1, jsonFormat2, jsonFormatN) is enough.

The commonly used product types are case classes, case objects, and tuples.

Simple generic case class

First, all formats for primitive types are imported from DefaultJsonProtocol.

The custom format for User type is defined using jsonFormat1 which lets spray-json know that User should be formatted as a sum of underlying types. The format is assigned to a value in this case, but it could be defined as a function — the library resolves both just the same.

To define a format for generic type Pair[T], the type parameter T should be passed to the format. Values do not accept generic type parameters, instead a function definition is used.

A format for Pairs[T] is generated similar to the User type. The only difference is that format for unknown type T should be available when serialization/deserialization operation is called. This requirement is set by adding a context bound for JsonFormat (typeDataFormat[T: JsonFormat]). When toJson is called on a Pair[User] object it accesses the format we defined in the context.

The code listing below demonstrates the solution.

An Abstract Generic Class with the Generic Type Parameter

Things change slightly when a specialized class is created by extending a class with a generic type parameter.

The key point in this example is that abstract class List needs to parse or convert a value of type T to JSON. List is not aware of type JSON format for type T. It requires a JsonFormat[T] to be provided along with type T by specifying a context bound for it (abstract class List[T: JsonFormat]).

Extending List[T] where T is Pair[Int] requires the support of JsonFormat[Pair[Int]] in the context, which is satisfied by importing CommonFormats.pairFormat,
which in turn requires the support of JsonFormat[Int] in the context,
which is satisfied by importing DefaultJsonProtocol._ where all primitive formats are defined.

Similarly, extending List[T] with T as Pair[User] requires the support of JsonFormat[Pair[User]] in the context,
which is satisfied by importing CommonFormats.pairFormat,
which in turn requires the support of JsonFormat[User] in the context,
which is satisfied by importing CommonFormats.userFormat.

The code listing below demonstrates the solution.

Separate Files Perspective

When all the code is clumped together it’s easy to lose the track of the dependency chain. Let’s separate the code into different files to isolate the dependencies. This example gives a better understanding of what imports are needed for each definition.

Anatomy of a JSON Format Definition Line

Let’s put the cornerstone code line under a magnifying glass.

Implicit — allows passing the format to toJson and convertTo methods.

IDE will hint “No implicits found for parameter writer: JsonWriter[Pair[T]]” without it.

Compilation fails with error “Cannot find JsonWriter or JsonFormat type class for com.useinsider.sprayjsongenericclass.nestedseparated.Pair[T]” and “not enough arguments for method toJson: … Unspecified value parameter writer”.

Even if the pairFormat is imported in the scope.

Def — used to employ the functions generic type parameter. The expression of type val pairFormat[T] is not a valid Scala expression at the moment.

[T] — pairFormat requires type T which is passed to jsonFormat2(Pair[T]). If not defined, it cannot resolve symbol T.

[T: JsonFormat] pairFormat requires context bound JsonFormat, in other words, it requires a JsonFormat[T] to be defined in the implicit context of the caller.

IDE will hint “No implicits found for evidence$5 JF[T]” if it is omitted.

Compilation fails with “could not find implicit value for evidence parameter of type spray.json.DefaultJsonProtocol.JF[T]” and “not enough arguments for method jsonFormat2”.

JsonFormat[Pair[T]] pairFormat can be used to serialize/deserialize values of type Pair[T]. When toJson and convertTo methods are called, they resolve the only JsonFormat[Pair[T]] available in the implicit context.

jsonFormat2(…) — generates a JsonFormat containing read/write methods for a case class/tuple with 2 fields. Uses other available formats in the scope — primitive formats from DefaultJsonProtocol._, custom formats in the same scope, and the provided format for type T (JsonFormat[T]).

Finally…

This concludes how to serialize with spray-json when using generics.

Happy coding!

--

--