Reading Sequences of Scala classes in Play JSON
One wise man said once: "Try to improve 1% every day so you will be an expert without you noticing". Following this advise, in my personal projects I try to use new libraries and this time the time has come for JSON with Play Framework. The objective of this post is to be able to read and write from a JSON file that contains just a list of items that can be maped to Scala classes.
This was not an easy task since it was directly an array and I found no way to solve the issue in the documentation. For reference completion, there is a way to do part if it (just the transformation from Seq[T]
to JSON) in Alvin Alexander's blog.
The objective
We want to be able to read and write the following JSON:
[
{"line":"R8","operador":"Generalitat/Renfe","lastStatusPubDate":1535953200000},
{"line":"R1","operador":"Generalitat/Renfe","lastStatusPubDate":1540744360000}
]
The issue
Since I decided to use a type class as an alias of Seq[Checkpoint]
, I was very confused in the way Play Framework's JSON implicits were being used. First of all, we define the case class format (for read and write). Just that is enough to transform a class instance to JSON (called write). It also allows us to write a Seq[Checkpoint]
. Great!
However, when I tried to read from a string and parse it to Seq[Checkpoint]
, I was unable to do so. I had run-time errors (e.g. play thought that I was getting a string with Checkpoint
but I was passing instead a Seq[Checkpoint]
) and compile-time errors when I defined a Format for Checkpoints
.
Error:(28, 70) No apply function found for scala.collection.Seq
implicit val checkpointsFormat: Format[Checkpoints] = Json.format[Checkpoints]
Error:(36, 54) ambiguous implicit values:
both value checkpointFormat of type play.api.libs.json.Format[cat.martsec.martsec.Checkpoint]
and value checkpointsFormat of type play.api.libs.json.Format[cat.martsec.martsec.Checkpoints]
match expected type play.api.libs.json.Reads[A]
val checkpointFromJson = Json.parse(stringValue).validate
Error:(36, 54) No Json deserializer found for type A. Try to implement an implicit Reads or Format for this type.
val checkpointFromJson = Json.parse(stringValue).validate
Error:(36, 54) not enough arguments for method validate: (implicit rds: play.api.libs.json.Reads[A])play.api.libs.json.JsResult[A].
Unspecified value parameter rds.
val checkpointFromJson = Json.parse(stringValue).validate
I solved some of them passing explicitly the format to the Json.parse("").validate(checkpointsFormat)
, however I was stuck with the first error shown above.
The solution
The was solved using Reads.seq()
and passing it explicitly to the validation method. See checkpointsRead
and its usage.
case class Checkpoint(line: String, operador: String, lastStatusPubDate: Long)
type Checkpoints = Seq[Checkpoint]
implicit val checkpointFormat: Format[Checkpoint] = Json.format[Checkpoint]
implicit val checkpointsRead: Reads[Seq[Checkpoint]] = Reads.seq(checkpointFormat)
val updatedCheckpoints = Seq(Checkpoint("R8","Generalitat/Renfe",1535953200000L),Checkpoint("R1","Generalitat/Renfe",1540744360000L))
val jsonCheckpoints = Json.toJson(updatedCheckpoints)
val stringValue = jsonCheckpoints.toString()
val checkpointFromJson = Json.parse(stringValue).validate(checkpointsRead)
checkpointFromJson match {
case r: JsResult[Checkpoints] => r.get
case _ =>
logger.error("Issue reading checkpoint file")
List[Checkpoint]()
}
It works like a charm:
[{"line":"R8","operador":"Generalitat/Renfe","lastStatusPubDate":1535953200000},{"line":"R1","operador":"Generalitat/Renfe","lastStatusPubDate":1540744360000}]
List(Checkpoint(R8,Generalitat/Renfe,1535953200000), Checkpoint(R1,Generalitat/Renfe,1540744360000))