Circe Encoder / Decoder for Subclass Types

Given the following ADT

sealed abstract class GroupRepository(val `type`: String) {
  def name: String
  def repositories: Seq[String]
  def blobstore: String
}
case class DockerGroup(name: String, repositories: Seq[String], blobstore: String = "default") extends GroupRepository("docker")
case class BowerGroup(name: String, repositories: Seq[String], blobstore: String = "default") extends GroupRepository("bower")
case class MavenGroup(name: String, repositories: Seq[String], blobstore: String = "default") extends GroupRepository("maven")

If the value is typeused to decode the instance for the instance.

How can I automatically (or semi-automatically) output encoders and decoders, I get the following behavior:

> println(MavenGroup("test", Seq("a", "b")).asJson.spaces2)
{
  "type" : "maven",
  "name" : "test",
  "repositories" : [
     "a",
     "b"
  ],
  "blobstore" : "default"
}
> println((MavenGroup("test", Seq("a", "b")): GroupRepository).asJson.spaces2)
{
  "type" : "maven",
  "name" : "test",
  "repositories" : [
     "a",
     "b"
  ],
  "blobstore" : "default"
}

Traditional execution approach

object GroupRepository {
  implicit val encoder = semiauto.deriveEncoder[GroupRepository]
  implicit val decoder = semiauto.deriveDecoder[GroupRepository]
}

Failure on two fronts:

  • It does not serialize meaning type.
  • Does not allow MavenGroup("test", Seq("a", "b")).asJson. It permits only the second alternative, which is MavenGroupfirst executed on GroupRepository.

The best solution I could come up with is:

object GroupRepository {
  implicit def encoder[T <: GroupRepository]: Encoder[T] = Encoder.instance(a => Json.obj(
    "type" -> Json.fromString(a.`type`),
    "name" -> Json.fromString(a.name),
    "repositories" -> Json.fromValues(a.repositories.map(Json.fromString)),
    "blobstore" -> Json.fromString(a.blobstore)
  ))
  implicit def decoder[T <: GroupRepository]: Decoder[T] = Decoder.instance(c =>
    c.downField("type").as[String].flatMap {
      case "docker" => c.as[DockerGroup](semiauto.deriveDecoder[DockerGroup])
      case "bower" => c.as[BowerGroup](semiauto.deriveDecoder[BowerGroup])
      case "maven" => c.as[MavenGroup](semiauto.deriveDecoder[MavenGroup])
    }.right.map(_.asInstanceOf[T])
  )
}

However, these were several drawbacks:

  • The encoder must be specified manually.
  • The decoder for each subtype is not cached, since it is necessary to explicitly pass the encoder.
+4

Source: https://habr.com/ru/post/1679982/


All Articles