如何使用Circe进行动态解码?

5

我的问题有点麻烦。我有一个看起来像这样的案例类

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

然而,我有两种输入类型。一种非常适合于案例类Foo,另一种缺少字段3和字段4,看起来像这样:{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],但是我们有很多代码是按照原始设计实现的,改变数据模型将引入大量的代码更改。因此我只想看看是否有任何临时解决方案。
非常感谢您!
2个回答

9
有多种方法可以实现这一点。我假设你不会从头开始构建编解码器并使用已经存在于circe中的内容。
默认参数 + generic-extras
有一个叫做 `circe-generic-extras` 的软件包,它允许对自动派生的编解码器进行一些定制。特别是,它允许您使用默认参数作为回退值。
缺点是它在编译时速度较慢,并且需要在范围内拥有一个隐式的 `io.circe.generic.extras.Configuration`。
所以,首先你需要那个隐式配置:
object Configs {
  implicit val useDefaultValues = Configuration.default.withDefaults
}

通常将此内容放入项目中的某个通用工具包中,以便您可以轻松地重用这些配置。

然后,在您的类上使用@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 点击此处

回退解码器

思路如下:使用单独的 case class 描述旧数据格式,并构建一个解码器来尝试将数据解析为新旧格式。Circe 解码器具有 or 组合器,可用于尝试此类解析。


在这里,首先描述了“旧”数据格式及其升级到新格式的方法:

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

同样的有效负载,这也会“正常工作”:

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

点击此处访问Scastie。


第一种方法需要额外的库,但少了很多样板代码。它还允许调用者提供 field4 但不提供 field3 - 在第二种方法中,如果出现这种情况,field4 的值将被完全忽略。

第二种方法可以处理比“加入默认值的字段”更复杂的变化,例如从几个其他值计算值或更改集合内部结构,并且在以后需要时也可以有多个版本。

哦,你还可以将 LegacyFoo 放入 object Foo 中,并使其私有化,如果你不想公开额外的数据类型。


3

这个问题可以使用参数默认值来解决吗?

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)

我正在尝试将HTTP响应解码为Foo case类。即使我们将默认值设置为field3,如果在响应中缺少它,Circe也会失败并显示错误Attemp to decode value on failed cursor - wtian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接