Akka-HTTP + Play-JSON Inheritance hierarchies

18 Sep
2020

This is mainly a note to self on how to build inheritance hierarchies (e.g. Animal -> [Cat / Dog] ) with Play-JSON.

import play.api.libs.json._
sealed trait Animal {
val `type`: String
}
case class Cat(name: String) extends Animal {
override val `type`: String = "cat"
}
case class Dog(name: String) extends Animal {
override val `type`: String = "dog"
}
trait AnimalJsonSupport
extends de.heikoseeberger.akkahttpplayjson.PlayJsonSupport {
implicit val catReads: Reads[Cat] =
(JsPath \ "name").read[String].map(Cat(_))
implicit val dogReads: Reads[Dog] =
(JsPath \ "name").read[String].map(Dog(_))
implicit val catWrites: Writes[Cat] = e =>
JsObject(Seq("type" > JsString("cat"), "name" > JsString(e.name)))
implicit val dogWrites: Writes[Dog] = e =>
JsObject(Seq("type" > JsString("dog"), "name" > JsString(e.name)))
implicit val animalReads: Reads[Animal] = Reads[Animal](jsValue => {
(jsValue \ "type").toEither
.map(
v =>
v.as[String] match {
case "cat" => catReads.map(_.asInstanceOf[Animal]).reads(jsValue)
case "dog" => dogReads.map(_.asInstanceOf[Animal]).reads(jsValue)
case t =>
JsError((JsPath \ "type"), "unkown animal type $t")
.asInstanceOf[JsResult[Animal]]
}
)
.fold(v => JsError(JsPath, v).asInstanceOf[JsResult[Animal]], v => v)
})
implicit val animalWrites: Writes[Animal] = Json.writes[Animal]
}
view raw Animal.scala hosted with ❤ by GitHub

import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import play.api.libs.json.Json
class AnimalJsonSupportSpec
extends AnyWordSpec
with Matchers
with AnimalJsonSupport
{
val catJsonString =
"""
|{
|"type":"cat",
|"name":"test"
|}
|""".stripMargin.replace("\n", "")
val dummyRequestResponse = Cat("test")
"The AnimalJsonSupport" when {
"handling the Cat-Type" should {
"correctly serialize the response" in {
Json.toJson(dummyRequestResponse).toString() should ===(catJsonString)
}
"correctly deserialize the cat response" in {
val parseResult = Json.fromJson[Animal](Json.parse(catJsonString))
parseResult.get should ===(dummyRequestResponse)
}
}
"handling the Dog-Type" should {
"correctly deserialize the dog response" in {
val json =
"""
|{
|"type":"dog",
|"name":"test2"
|}
|""".stripMargin.replace("\n", "")
val parseResult = Json.fromJson[Animal](Json.parse(json))
parseResult.get should ===(Dog("test2"))
}
}
"handling other types" should {
"fail trying to deserialize a unknown type" in {
val json = """
|{
|"type":"unknown",
|"name":"test"
|}
|""".stripMargin.replace("\n", "")
val result = Json.fromJson[Animal](Json.parse(json))
result.isError shouldBe(true)
}
}
}
}
view raw AnimalJsonSpec.scala hosted with ❤ by GitHub

Documentation can be found here:

Comment Form

top