代码之家  ›  专栏  ›  技术社区  ›  Travis Brown

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

  •  15
  • Travis Brown  · 技术社区  · 7 年前

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

    [ "Foo", "McBar", true, false, false, false, true, 137 ]
    

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

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

    哪种方法有效:

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

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

    当然有一种方法可以做到这一点,它不涉及在游标和 Json 价值观,在我们的错误信息中丢弃历史记录,而通常只是成为一个眼中钉?


    一些背景:关于在circe中编写这样的自定义JSON数组解码器的问题经常出现(例如。 this morning this experimental project 对于一些细节),所以我真的不想花太多时间在文档中添加这样的示例,但它足够了,我认为它确实值得堆栈溢出Q&A.

    1 回复  |  直到 7 年前
        1
  •  23
  •   Travis Brown    7 年前

    使用光标

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

    这也适用于:

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

    但它也为我们指明了错误发生的地方:

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

    关键思想是我们交错“读取”操作(即 .as[X] downArray 还有三个 delete 方法调用)。

    c 是一个 HCursor 我们希望它指向一个数组。 c.downArray 将光标移动到数组中的第一个元素。如果输入根本不是数组,或者是空数组,则此操作将失败,我们将得到一条有用的错误消息。如果成功,则 for

    中的第二行 对于 删去 方法名称的一部分并不意味着它实际上在变异任何东西circe中的任何东西都不会以用户可以观察到的任何方式变异任何东西,这只是意味着该元素将不可用于对结果游标的任何未来操作。

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

    下一行可能是最有趣的:

        stuffC  = ageC.delete
    

    这意味着,好的,我们已经到了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)
    

    val bad = """[["Foo"], "McBar", true, "true", false, 13.7 ]"""
    
    val badResult = io.circe.jawn.decodeAccumulating[Foo](bad)
    

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