9

鉴于以下 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")

其中值type用于解码要实例化的实例。

如何自动(或半自动)派生编码器和解码器,以便获得以下行为:

> 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"
}

传统的做法

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

在两个方面失败:

  • 它不序列化值type
  • 不允许MavenGroup("test", Seq("a", "b")).asJson。它只允许第二个选择 where MavenGroupis first cast to GroupRepository

我能想出的最佳解决方案是:

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])
  )
}

但是它有几个缺点:

  • 编码器是手动指定的。
  • 每个子类型的解码器没有被缓存,因为需要显式传递编码器。
4

1 回答 1

1

您可以为每个案例类定义编码器/解码器(编解码器),因为val它不会每次都创建:

import io.circe.generic.semiauto.deriveCodec
import io.circe.Codec

private implicit val dockerCodec: Codec.AsObject[DockerGroup] = deriveCodec[DockerGroup]
private implicit val bowerCodec: Codec.AsObject[BowerGroup] = deriveCodec[BowerGroup]
private implicit val mvnCodec: Codec.AsObject[MavenGroup] = deriveCodec[MavenGroup]

方法1:类似于编解码器的结构

我认为没有办法做这样的通用编解码器,isInstanceOf但我会弄错。

所以,我会为一些T使用具体编解码器的编解码器定义取决于type字段:

import io.circe.Decoder.Result
import io.circe.generic.semiauto.deriveCodec
import io.circe.{Codec, HCursor, Json}
import io.circe.syntax._


object Codecs {
  private implicit val dockerCodec: Codec.AsObject[DockerGroup] = deriveCodec[DockerGroup]
  private implicit val bowerCodec: Codec.AsObject[BowerGroup] = deriveCodec[BowerGroup]
  private implicit val mvnCodec: Codec.AsObject[MavenGroup] = deriveCodec[MavenGroup]
  
  implicit def codec[T <: GroupRepository]: Codec[T] = new Codec[T] {
    override def apply(a: T): Json =
      (a match {
        case d: DockerGroup => d.asInstanceOf[DockerGroup].asJsonObject
        case b: BowerGroup => b.asInstanceOf[BowerGroup].asJsonObject
        case m: MavenGroup => m.asInstanceOf[MavenGroup].asJsonObject
      }).+:("type", a.`type`.asJson).asJson

    override def apply(c: HCursor): Result[T] = c.downField("type").as[String].flatMap {
      case "docker" => c.as[DockerGroup]
      case "bower" => c.as[BowerGroup]
      case "maven" => c.as[MavenGroup]
    }.map(_.asInstanceOf[T])
  }
}

object Test extends App {

  import Codecs._
  println(DockerGroup("doc", Seq("a", "b")).asJson)
  println(DockerGroup("doc", Seq("a", "b")).asJson.as[DockerGroup])

  println(BowerGroup("bow", Seq("a", "b")).asJson)
  println(BowerGroup("bow", Seq("a", "b")).asJson.as[BowerGroup])

  println(MavenGroup("mvn", Seq("a", "b")).asJson)
  println(MavenGroup("mvn", Seq("a", "b")).asJson.as[MavenGroup])
}

输出:

{
  "type" : "docker",
  "name" : "doc",
  "repositories" : [
    "a",
    "b"
  ],
  "blobstore" : "default"
}
Right(DockerGroup(doc,List(a, b),default))
{
  "type" : "bower",
  "name" : "bow",
  "repositories" : [
    "a",
    "b"
  ],
  "blobstore" : "default"
}
Right(BowerGroup(bow,List(a, b),default))
{
  "type" : "maven",
  "name" : "mvn",
  "repositories" : [
    "a",
    "b"
  ],
  "blobstore" : "default"
}
Right(MavenGroup(mvn,List(a, b),default))

方法2:摆脱type字段并使用可配置派生

我们可以在我们的解析 JSON中定义鉴别器字段,并在父级中去掉这个字段(它现在可以是一个特征)。在这里,我使用一些临时鉴别器来获得结果 json 中字段的类似结果,但我认为在这种方法中它可能更优雅:typeConfigurationabstract class GroupRepositorytype

constructorName => constructorName.toLowerCase.dropRight("Group".length)

请记住,要使用ConfiguredJsonCodec注释,您应该implicit val config: Configuration在伴随对象中定义。此外,您应该-Ymacro-annotations为宏的 scala 编译器添加标志:

scalacOptions ++= Seq("-Ymacro-annotations")

完整代码:

import io.circe.Decoder.Result
import io.circe.generic.extras.{Configuration, ConfiguredJsonCodec}
import io.circe.syntax._
import io.circe.{Codec, HCursor, Json}
import ru.hardmet.GroupRepository._

@ConfiguredJsonCodec
sealed trait GroupRepository {
  def name: String

  def repositories: Seq[String]

  def blobstore: String
}

case class DockerGroup(name: String, repositories: Seq[String], blobstore: String = "default")
  extends GroupRepository

case class BowerGroup(name: String, repositories: Seq[String], blobstore: String = "default")
  extends GroupRepository

case class MavenGroup(name: String, repositories: Seq[String], blobstore: String = "default")
  extends GroupRepository

object GroupRepository {
  implicit val config: Configuration =
    Configuration.default
      .withDiscriminator("type").copy(
      transformConstructorNames = _.toLowerCase.dropRight("Group".length)
    )
}

object GroupRepositoryCodec {
  implicit def codec[T <: GroupRepository]: Codec[T] = new Codec[T] {
    override def apply(a: T): Json = a.asInstanceOf[GroupRepository].asJson

    override def apply(c: HCursor): Result[T] = c.as[GroupRepository].map(_.asInstanceOf[T])
  }
}

object JsonExperiments extends App {

  import GroupRepositoryCodec._

  println(DockerGroup("doc", Seq("a", "b")).asJson)
  println(DockerGroup("doc", Seq("a", "b")).asJson.as[DockerGroup])

  println(BowerGroup("bow", Seq("a", "b")).asJson)
  println(BowerGroup("bow", Seq("a", "b")).asJson.as[BowerGroup])

  println(MavenGroup("mvn", Seq("a", "b")).asJson)
  println(MavenGroup("mvn", Seq("a", "b")).asJson.as[MavenGroup])
}

输出将相同但有所不同 - typeJSON 中的字段位于对象的末尾:

{
  "name" : "doc",
  "repositories" : [
    "a",
    "b"
  ],
  "blobstore" : "default",
  "type" : "docker"
}
于 2021-09-03T08:39:26.927 回答