使用Scala中的circe解码结构化JSON数组

Tra*_*own 13 json scala circe

假设我需要解码如下所示的JSON数组,其中开头有几个字段,一些任意数量的同类元素,然后是其他一些字段:

[ "Foo", "McBar", true, false, false, false, true, 137 ]
Run Code Online (Sandbox Code Playgroud)

我不知道为什么有人会选择像这样编码他们的数据,但人们做了奇怪的事情,并且假设在这种情况下我只需要处理它.

我想将这个JSON解码为这样的案例类:

case class Foo(firstName: String, lastName: String, age: Int, stuff: List[Boolean])
Run Code Online (Sandbox Code Playgroud)

我们可以这样写:

import cats.syntax.either._
import io.circe.{ Decoder, DecodingFailure, Json }

implicit val fooDecoder: Decoder[Foo] = Decoder.instance { c =>
  c.focus.flatMap(_.asArray) match {
    case Some(fnJ +: lnJ +: rest) =>
      rest.reverse match {
        case ageJ +: stuffJ =>
          for {
            fn    <- fnJ.as[String]
            ln    <- lnJ.as[String]
            age   <- ageJ.as[Int]
            stuff <- Json.fromValues(stuffJ.reverse).as[List[Boolean]]
          } yield Foo(fn, ln, age, stuff)
        case _ => Left(DecodingFailure("Foo", c.history))
      }
    case None => Left(DecodingFailure("Foo", c.history))
  }
}
Run Code Online (Sandbox Code Playgroud)

......有效:

scala> fooDecoder.decodeJson(json"""[ "Foo", "McBar", true, false, 137 ]""")
res3: io.circe.Decoder.Result[Foo] = Right(Foo(Foo,McBar,137,List(true, false)))
Run Code Online (Sandbox Code Playgroud)

但是,这太可怕了.此外,错误消息完全无用:

scala> fooDecoder.decodeJson(json"""[ "Foo", "McBar", true, false ]""")
res4: io.circe.Decoder.Result[Foo] = Left(DecodingFailure(Int, List()))
Run Code Online (Sandbox Code Playgroud)

当然有一种方法可以做到这一点,不涉及在游标和Json值之间来回切换,在我们的错误消息中丢弃历史,而且通常只是一个眼睛?


一些上下文:关于编写这样的自定义JSON数组解码器的问题经常出现(例如今天早上).有关如何执行此操作的具体细节可能会在即将发布的circe版本中发生变化(虽然API类似; 有关详细信息,请参阅此实验项目),因此我并不想花费大量时间添加这样的示例就像文档一样,但它足以让我觉得它确实值得Stack Overflow问答.

Tra*_*own 16

使用游标

有一个更好的方法!您可以通过直接使用游标一直保持有用的错误消息来更简洁地编写这些内容:

case class Foo(firstName: String, lastName: String, age: Int, stuff: List[Boolean])

import cats.syntax.either._
import io.circe.Decoder

implicit val fooDecoder: Decoder[Foo] = Decoder.instance { c =>
  val fnC = c.downArray

  for {
    fn     <- fnC.as[String]
    lnC     = fnC.deleteGoRight
    ln     <- lnC.as[String]
    ageC    = lnC.deleteGoLast
    age    <- ageC.as[Int]
    stuffC  = ageC.delete
    stuff  <- stuffC.as[List[Boolean]]
  } yield Foo(fn, ln, age, stuff)
}
Run Code Online (Sandbox Code Playgroud)

这也有效:

scala> fooDecoder.decodeJson(json"""[ "Foo", "McBar", true, false, 137 ]""")
res0: io.circe.Decoder.Result[Foo] = Right(Foo(Foo,McBar,137,List(true, false)))
Run Code Online (Sandbox Code Playgroud)

但它也告诉我们错误发生的地方:

scala> fooDecoder.decodeJson(json"""[ "Foo", "McBar", true, false ]""")
res1: io.circe.Decoder.Result[Foo] = Left(DecodingFailure(Int, List(DeleteGoLast, DeleteGoRight, DownArray)))
Run Code Online (Sandbox Code Playgroud)

它也更短,更具声明性,并且不需要不可读的嵌套.

这个怎么运作

关键的想法是我们将"读取"操作(.as[X]光标上的调用)与导航/修改操作(downArray以及三个delete方法调用)交错.

当我们开始,cHCursor我们在一个阵列希望点.c.downArray将光标移动到数组中的第一个元素.如果输入根本不是数组,或者是一个空数组,则此操作将失败,我们将收到有用的错误消息.如果成功,for-comprehension 的第一行将尝试将第一个元素解码为字符串,并将光标指向第一个元素.

for-comprehension中的第二行说"好吧,我们已经完成了第一个元素,所以让我们忘记它并转移到第二个元素".delete方法名称的一部分并不意味着它实际上正在改变任何东西 - circe中的任何内容都不会以用户可以观察到的任何方式改变任何东西 - 它只是意味着该元素将不会对结果游标的任何将来操作可用.

第三行尝试将原始JSON数组中的第二个元素(现在是新游标中的第一个元素)解码为字符串.完成后,第四行"删除"该元素并移动到数组的末尾,然后第五行尝试将该最终元素解码为Int.

下一行可能是最有趣的:

    stuffC  = ageC.delete
Run Code Online (Sandbox Code Playgroud)

这说,好吧,我们在JSON数组的修改视图中的最后一个元素(之前我们删除了前两个元素).现在我们删除最后一个元素,并将光标移动起来,这样它指向整个(修改)阵列,然后我们可以解码为布尔值的列表,我们就大功告成了.

更多的错误积累

实际上你可以写一个更简洁的方法:

import cats.syntax.all._
import io.circe.Decoder

implicit val fooDecoder: Decoder[Foo] = (
  Decoder[String].prepare(_.downArray),
  Decoder[String].prepare(_.downArray.deleteGoRight),
  Decoder[Int].prepare(_.downArray.deleteGoLast),
  Decoder[List[Boolean]].prepare(_.downArray.deleteGoRight.deleteGoLast.delete)
).map4(Foo)
Run Code Online (Sandbox Code Playgroud)

这也可以工作,并且它还有一个额外的好处,即如果多个成员的解码失败,您可以同时获得所有故障的错误消息.例如,如果我们有这样的东西,我们应该期待三个错误(对于非字符串名字,非整数年龄和非布尔填充值):

val bad = """[["Foo"], "McBar", true, "true", false, 13.7 ]"""

val badResult = io.circe.jawn.decodeAccumulating[Foo](bad)
Run Code Online (Sandbox Code Playgroud)

这就是我们所看到的(连同每个故障的具体位置信息):

scala> badResult.leftMap(_.map(println))
DecodingFailure(String, List(DownArray))
DecodingFailure(Int, List(DeleteGoLast, DownArray))
DecodingFailure([A]List[A], List(MoveRight, DownArray, DeleteGoParent, DeleteGoLast, DeleteGoRight, DownArray))
Run Code Online (Sandbox Code Playgroud)

您应该更喜欢这两种方法中的哪一种是品味问题以及您是否关心错误累积 - 我个人认为第一种方法更具可读性.