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

wan*_*ngt 4 scala circe

我的问题有点棘手。我有一个像这样的案例类

case class Foo(
    id: String,
    name: String,
    field1: Boolean,
    field2: Boolean,
    field3: Boolean,
    field4: Boolean
)
Run Code Online (Sandbox Code Playgroud)

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

例如,

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

Foo("Test, "Test", true, true, false, flase)
Run Code Online (Sandbox Code Playgroud)

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

Foo("Test, "Test", true, true, true, flase)
Run Code Online (Sandbox Code Playgroud)

我知道最好的解决方案是 setfield3field4as Option[Boolean],但是我们按照原始设计实现了大量代码,更改数据模型将引入大量代码更改。所以只想看看是否有任何 make shift 解决方案。

非常感谢!

Ole*_*cov 6

有多种方法可以做到这一点。我将假设您不会从头开始构建编解码器并使用您可以从已有的内容中获得的内容。

默认参数 + generic-extras

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

缺点是编译速度稍慢,并且还要求您具有隐式io.circe.generic.extras.Configuration作用域。


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

object Configs {
  implicit val useDefaultValues = Configuration.default.withDefaults
}
Run Code Online (Sandbox Code Playgroud)

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

然后,你@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
)
Run Code Online (Sandbox Code Playgroud)

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

could not find Lazy implicit value of type io.circe.generic.extras.codec.ConfiguredAsObjectCodec[Foo]
Run Code Online (Sandbox Code Playgroud)

Foo如果缺少默认值的字段,现在就可以解码了:

could not find Lazy implicit value of type io.circe.generic.extras.codec.ConfiguredAsObjectCodec[Foo]
Run Code Online (Sandbox Code Playgroud)

Self-contained scastie here.

Fallback decoder

The idea is as follows: have a separate case class describing the old format of data, and build a decoder to attempt parsing the data as both old and new formats. Circe decoders have or combinator for just that sort of attempting.


Here, first you describe the "old" format of data, and a way to upgrade it to a new one:

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

With new format, you have to join the codecs manually, so you can't use macro annotation. Still, you can use generic.semiauto.deriveXXX methods to not have to list all the fields yourself:

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

This will also "just work" for the same payload:

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

Scastie here.


The first approach requires an extra library, but has less boilerplate. It will also allow the caller to supply, e.g. field4 but not field3 - in second approach, the value of field4 will be entirely discarded in that scenario.

The second one allows to handle more complicated changes than "field added with a default values", like computing values out of several others or changing the structure inside a collection, and also to have several more versions should you need them later.

Oh, you can also put LegacyFoo into object Foo, and make it private if you don't want extra public datatypes exposed.