[英]Decoding structured JSON arrays with circe in Scala
假设我需要对如下所示的JSON数组进行解码,其中在开始处有几个字段,一些任意数量的齐次元素,然后是其他字段:
[ "Foo", "McBar", true, false, false, false, true, 137 ]
我不知道为什么有人会选择像这样对他们的数据进行编码,但是人们做的事情很奇怪,并且在这种情况下,我只需要处理它。
我想将此JSON解码为如下的case类:
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
值之间来回切换,不丢弃错误消息中的历史记录,而只是使人眼花?乱?
一些上下文:关于编写这样的自定义JSON数组解码器的问题大约经常出现(例如, 今天早上 )。 在即将发布的circe版本中,如何执行此操作的具体细节可能会发生变化(尽管API将会类似;有关更多详细信息,请参见此实验项目 ),因此,我真的不想花很多时间来添加文档中有这样的示例,但是我认为它确实值得进行堆栈溢出问答。
有一个更好的方法! 通过直接使用游标,您可以更加简洁地编写代码,同时还可以维护有用的错误消息:
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
方法调用)进行downArray
。
当我们开始时, c
是我们希望指向数组的HCursor
。 c.downArray
将光标移动到数组中的第一个元素。 如果输入根本不是数组或为空数组,则此操作将失败,并且我们将收到一条有用的错误消息。 如果成功, for
-comprehension的第一行将尝试将第一个元素解码为字符串,并使光标指向该第一个元素。
for
-comprehension中的第二行表示“好的,我们已经完成了第一个元素,因此让我们忘记它,然后移至第二个。” 方法名称的delete
部分并不意味着它实际上是在改变任何东西-几乎没有任何改变以用户可以观察到的任何方式-只是意味着该元素将不能再用于结果游标上的任何将来的操作。
第三行尝试将原始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))
您应该采用这两种方法中的哪一种取决于您的口味,以及您是否关心错误累积—我个人认为第一种方法更具可读性。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.