Spray-json in Scala for Case Class Serialization
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!