Предположим, мне нужно декодировать массивы 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, возникают довольно часто (например, этим утром ). Конкретные детали того, как это сделать, вероятно, изменятся в следующей версии circe (хотя API будет аналогичным; см. этот экспериментальный проект для некоторых деталей), поэтому я действительно не хочу тратить много времени на добавление такого примера в документацию, но его достаточно, чтобы я думаю, что он заслуживает переполнения стека Вопросы и ответы.
Ответ 1 · рейтинг 24 · Travis Brown
Работа с курсорами
Существует лучший способ! Вы можете написать это намного более кратко, сохраняя при этом полезные сообщения об ошибках, работая напрямую с курсорами на всем протяжении:
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-compution попытается декодировать этот первый элемент в строку и оставит наш курсор, указывающий на этот первый элемент.
Во второй строке for-computing написано: "Хорошо, мы закончили с первым элементом, так что давайте забудем об этом и перейдем ко второму". Часть delete в имени метода не означает, что он на самом деле что-либо изменяет - ничто в 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))
Какой из этих двух подходов вам следует предпочесть, зависит от вашего вкуса и от того, заботитесь ли вы о накоплении ошибок. Лично я считаю, что первый немного более читабелен.