如何使用Circe做动态解码?

How to use Circe to do a dynamic decoding?

我的问题有点棘手。我有一个案例 class 看起来像这样

case class Foo(
    id: String,
    name: String,
    field1: Boolean,
    field2: Boolean,
    field3: Boolean,
    field4: Boolean
)

但是,我有两种输入方式,一种非常适合class Foo。另一个缺少 field3field4 的值,看起来像 {id: "Test", name: "Test", field1: true, field2: true},我想创建一个适用于这两种情况的 Decoder[Foo],如果缺少输入 field3field4,只需设置默认值 false。那可能吗?

例如,

(1) 对于输入 {id: "Test", name: "Test", field1: true, field2: true},我想将其解码为

Foo("Test, "Test", true, true, false, flase)

(2) 对于输入{id: "Test", name: "Test", field1: true, field2: true, field3: true, field4: false},我想把它解码成

Foo("Test, "Test", true, true, true, flase)

我知道最好的解决方案是将 field3field4 设置为 Option[Boolean],但是我们有大量代码按照原始设计实现,并且更改数据模型将介绍了很多代码更改。所以只想看看有没有什么make shift的解决办法。

非常感谢!

参数默认值对此不起作用吗?

case class Foo(id: String
              ,name: String
              ,field1: Boolean
              ,field2: Boolean
              ,field3: Boolean = false
              ,field4: Boolean = false)

Foo("Jo","Josephine",true,true)
//res0: Foo = Foo(Jo,Josephine,true,true,false,false)

有多种方法可以做到这一点。我假设您不会从头开始构建编解码器,也不会使用您可以从现有资源中获得的资源。

默认参数 + generic-extras

circe-generic-extras 包,它允许对自动派生的编解码器进行一些自定义。特别是,它确实允许您使用默认参数作为后备值。

缺点是编译速度稍慢,并且还需要您在范围内有一个隐式 io.circe.generic.extras.Configuration


所以,首先你需要隐式配置:

object Configs {
  implicit val useDefaultValues = Configuration.default.withDefaults
}

这通常会进入您项目中的一些通用 util 包,因此您可以轻松地重用这些配置。

然后,您在 class 上使用 @ConfiguredJsonCodec 宏注释,或者在它的同伴中使用 extras.semiauto.deriveConfiguredCodec

import Configs.useDefaultValues

@ConfiguredJsonCodec
case class Foo(
    id: String,
    name: String,
    field1: Boolean,
    field2: Boolean,
    field3: Boolean = false,
    field4: Boolean = false
)

重要的是不要忘记配置导入,并且不要同时导入多个配置。否则你会得到一个无用的错误,比如

could not find Lazy implicit value of type io.circe.generic.extras.codec.ConfiguredAsObjectCodec[Foo]

这足以解码 Foo,以防缺少默认值的字段:

println {
    io.circe.parser.decode[Foo]("""
{
  "id": "someid",
  "name": "Gordon Freeman",
  "field1": false,
  "field2": true
}
""")
  }

独立的 scastie here.

后备解码器

想法如下:有一个单独的案例 class 描述数据的旧格式,并构建一个解码器以尝试将数据解析为旧格式和新格式。 Circe 解码器有 or 组合器来进行这种尝试。


这里先介绍一下数据的"old"格式,以及升级到新格式的方法:

@JsonCodec(decodeOnly = true)
case class LegacyFoo(
    id: String,
    name: String,
    field1: Boolean,
    field2: Boolean,
) {
  def upgrade: Foo =
    Foo(id, name, field1, field2, false, false)
}

使用新格式,您必须手动加入编解码器,因此无法使用宏注释。不过,您可以使用 generic.semiauto.deriveXXX 方法来不必自己列出所有字段:

case class Foo(
    id: String,
    name: String,
    field1: Boolean,
    field2: Boolean,
    field3: Boolean,
    field4: Boolean
)

object Foo {
  implicit val encoder: Encoder[Foo] = semiauto.deriveEncoder[Foo]
  implicit val decoder: Decoder[Foo] =
    semiauto.deriveDecoder[Foo] or Decoder[LegacyFoo].map(_.upgrade)
}

对于相同的负载,这也将 "just work":

println {
    io.circe.parser.decode[Foo]("""
{
  "id": "someid",
  "name": "Gordon Freeman",
  "field1": false,
  "field2": true
}
""")
}

斯卡斯蒂 here.


第一种方法需要额外的库,但样板文件较少。它还将允许调用者提供,例如field4 但不是 field3 - 在第二种方法中, field4 的值将在这种情况下被完全丢弃。

第二个允许处理比 "field added with a default values" 更复杂的更改,例如计算其他几个值或更改集合内部的结构,并且还可以有多个版本,以备日后需要时使用。

哦,你也可以将 LegacyFoo 放入 object Foo,如果你不想暴露额外的 public 数据类型,则将其设为私有。