使用 Argonaut 或 Circe 从不完整的 JSON 更新案例类

Posted

技术标签:

【中文标题】使用 Argonaut 或 Circe 从不完整的 JSON 更新案例类【英文标题】:Update case class from incomplete JSON with Argonaut or Circe 【发布时间】:2017-01-11 12:57:06 【问题描述】:

如果 json 不完整(缺少某些字段),我需要从案例类实例创建一个更新的实例(任何需要的 DecodeJsons 隐式派生)。如何使用 Argonaut(最好)或 Circe(如果必须的话)来实现这一点?

例子:

case class Person(name:String, age:Int)
val person = Person("mr complete", 42)
val incompletePersonJson = """"name":"mr updated""""
val updatedPerson = updateCaseClassFromIncompleteJson(person, incompletePersonJson)

println(updatedPerson)
//yields Person(mr updated, 42) 

我很确定我必须将 json 解析为 json AST,然后将其转换为 Shapeless LabelledGeneric,然后以某种方式使用 Shapeless 更新来更新案例类实例。


编辑 2

在阅读了 Shapeless 源代码后,我发现我可以生成自己的“默认”对象。我设法创建了一个解决方案,该解决方案需要在解析 json 时存在案例类的实例。我希望避免这种情况,而是稍后提供实例。无论如何,它是:

import shapeless._
import argonaut._
import ArgonautShapeless._
import shapeless.ops.hlist.Mapper

case class Person(name: String, age: Int)

object MkDefault 

  object toSome extends Poly1 
    implicit def default[P] = at[P](Some(_))
  

  def apply[P, L <: HList, D <: HList]
  (p: P)
  (implicit
   g: Generic.Aux[P, L],
   mpr: Mapper.Aux[toSome.type, L, D]
  ): Default.Aux[P, mpr.Out] =
    Default.mkDefault[P, D](mpr(g.to(p)))



object Testy extends App 
    implicit val defs0 = MkDefault(Person("new name? NO", 42))
    implicit def pd = DecodeJson.of[Person]
    val i = """"name":"Old Name Kept""""
    val pp = Parse.decodeOption[Person](i).get
    println(pp)

这会产生Person(Old Name Kept,42)

【问题讨论】:

调试 ArgonautShapeless' DecodeJson 推断 (ArgonautShapeless.derivedDecodeJson),我看到一个对象 defaults=Defaults$AsOptions$$anon$9 用值 None :: None :: HNil 实例化。对我来说,如果我能以某种方式用我自己提供的隐式实例替换它,我可以默认以某种方式填充缺失的 json。 我能想到的唯一能以类型安全的方式实现的是将现有案例类重新序列化为 json,将两者都转换为 Map[String, Any],然后合并映射,转换回 json,然后重新解析 @Falmarri 这实际上并不是一个坏主意,我会记住它作为备份解决方案,因为它显然需要更多的时间和计算机资源。 【参考方案1】:

为了完整起见:自 0.2 版本以来,大约已提供对此类“修补”实例的支持:

import io.circe.jawn.decode, io.circe.generic.auto._

case class Person(name: String, age: Int)

val person = Person("mr complete", 42)
val incompletePersonJson = """"name":"mr updated""""

val update = decode[Person => Person](incompletePersonJson)

然后:

scala> println(update.map(_(person)))
Right(Person(mr updated,42))

我关于这项技术的原始blog post 使用了 Argonaut(主要是因为我在开始研究 circe 前几个月写了它),并且该实现是 available as a library,尽管我从未在任何地方发布过。

【讨论】:

这正是我想要的。现在在我的项目中从 Argonaut 切换到 Circe。很棒的工作,继续努力:D 在使用 Circe 从 HTTP PATCH 回到 HTTP POST 测试后,注意到与 Argonaut 的显着差异:在 Circe 中完全忽略案例类的默认值,而在 Argonaut 中,如果 json 和有一个默认值,它可以解码。还注意到 Circe 中的一个未解决问题:github.com/travisbrown/circe/issues/65想知道它是否计划在不久的将来发布?【参考方案2】:

您可以在Person 上生成带有宏注释的implicit val defs / pd(例如,在object Person 中,并执行import Person._ 来召唤隐含)。有关用法示例,请参阅 this unfinished Simulacrum in scalameta(scala-reflect 也很好,但似乎 scalameta 在这里就足够了)。您还必须在某处指定缺少的默认值(42),例如,以防类构造函数(age: Int = 42,识别也可以在宏中完成)。

【讨论】:

谢谢。我希望避免创建自己的宏,因为它们通常会演变为维护地狱,但我还没有研究过 scalameta 或 Simulacrum。我今天将保持悬赏开放,以防有人为 circe 或 argonaut 发布非自定义宏无形解决方案。如果没有回复,我就给你 50 的努力 :) 我正在编写更详细的基于 Scalameta 的宏注释指南。希望在〜周内完成。 “simulacrum-meta”只是 scalameta 的 API 的一个例子。一般来说,如果 scalameta 满足要求(例如,尚不支持语义 API),则应该优先于 scala-reflect。在这种情况下,我认为 scalameta 应该可以完成这项工作。

以上是关于使用 Argonaut 或 Circe 从不完整的 JSON 更新案例类的主要内容,如果未能解决你的问题,请参考以下文章

尝试为ADT编写Circe编码器或解码器时遇到错误

Circe 找不到隐式编码器

使用 circe 将 Scala None 编码为 JSON 值

Scala使用circe将None编码为NaN json值

使用 circe 解码 JSON 对象时捕获未使用的字段

如何使用 circe 将密封特征案例对象转换为字符串