用于子类类型的Circe编码器/解码器

Sim*_*ins 8 scala circe

鉴于以下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")
Run Code Online (Sandbox Code Playgroud)

其中值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"
}
Run Code Online (Sandbox Code Playgroud)

做传统的做法

object GroupRepository {
  implicit val encoder = semiauto.deriveEncoder[GroupRepository]
  implicit val decoder = semiauto.deriveDecoder[GroupRepository]
}
Run Code Online (Sandbox Code Playgroud)

在两个方面失败:

  • 它不会序列化该值type.
  • 不允许MavenGroup("test", Seq("a", "b")).asJson.它只允许MavenGroup第一个转换到的第二个替代方案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])
  )
}
Run Code Online (Sandbox Code Playgroud)

但是它有几个缺点:

  • 编码器是手动指定的.
  • 由于必须明确地传递编码器,因此不缓存每个子类型的解码器.

Bor*_*nov 2

您可以为每个案例类定义编码器/解码器(编解码器),因为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]
Run Code Online (Sandbox Code Playgroud)

方法 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])
}
Run Code Online (Sandbox Code Playgroud)

输出:

{
  "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))
Run Code Online (Sandbox Code Playgroud)

方法 2:摆脱type字段并使用可配置的推导

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

{
  "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))
Run Code Online (Sandbox Code Playgroud)

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

constructorName => constructorName.toLowerCase.dropRight("Group".length)
Run Code Online (Sandbox Code Playgroud)

完整代码:

scalacOptions ++= Seq("-Ymacro-annotations")
Run Code Online (Sandbox Code Playgroud)

输出将是相同的,但有区别 - typeJSON 中的字段位于对象的末尾:

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])
}
Run Code Online (Sandbox Code Playgroud)