Как полуавтоматически получить декодер для списка некоторого типа с помощью Circe? - PullRequest
1 голос
/ 30 апреля 2020

У меня есть неявный класс, который декодирует ответ сервера в JSON и последний в правильном классе, чтобы избежать повторных вызовов к .as и .getOrElse во всех тестах:

  implicit class RouteTestResultBody(testResult: RouteTestResult) {
    def body: String = bodyOf(testResult)
    def decodedBody[T](implicit d: Decoder[T]): T =
      decode[Json](body)
        .fold(err => throw new Exception(s"Body is not a valid JSON: $body"), identity)
        .as[T]
        .getOrElse(throw new Exception(s"JSON doesn't have the right shape: $body"))
  }

Конечно, он полагается на то, что мы передаем декодер:

import io.circe.generic.semiauto.deriveDecoder

val result: RouteTestResult = ...
result.decodedBody(deriveDecoder[SomeType[AnotherType])

Он работает большую часть времени, но не работает, когда ответом является список:

result.dedoceBody(deriveDecoder[List[SomeType]])
// throws "JSON doesn't have the right shape"

Как полуавтоматически получить декодер для списка с указанными c типами внутри?

1 Ответ

0 голосов
/ 11 мая 2020

Терминология здесь, к сожалению, перегружена, так как мы используем «получение» в двух смыслах:

  • Предоставление экземпляра, например, List[A], заданного экземпляра для A.
  • Предоставление экземпляра для класса дел или иерархии запечатанных признаков, заданных экземпляров для всех типов элементов.

Эта проблема не указана c для Цирцеи или даже Scala. При написании статьи о Circe я обычно стараюсь вообще не ссылаться на первый тип генерации экземпляров как на «деривацию», а на второй тип - на «generi c дифференцирование», чтобы подчеркнуть, что мы генерируем экземпляры через generi. c представление типа данных algebrai c.

Тот факт, что мы иногда используем одно и то же слово для ссылки на оба типа генерации экземпляров класса типов, является проблемой, поскольку они обычно очень разные механизмы в Scala. В Circe то, что предоставляет экземпляр кодера или декодера для List[A], заданное для A, - это метод в объекте-компаньоне класса типа. Например, в object Decoder в circe-core у нас есть метод, подобный этому:

implicit def decodeList[A](implicit decodeA: Decoder[A]): Decoder[List[A]] = ...

Поскольку это определение метода содержится в объекте-компаньоне Decoder, если вы запрашиваете неявный Decoder[List[A]] в контексте, где у вас есть неявное Decoder[A], компилятор найдет и использует decodeList. Вам не нужно ни импорта, ни дополнительных определений. Например:

scala> case class Foo(i: Int)
class Foo

scala> import io.circe.Decoder, io.circe.parser
import io.circe.Decoder
import io.circe.parser

scala> implicit val decodeFoo: Decoder[Foo] = Decoder[Int].map(Foo(_))
val decodeFoo: io.circe.Decoder[Foo] = io.circe.Decoder$$anon$1@6e992c05

scala> parser.decode[List[Foo]]("[1, 2, 3]")
val res0: Either[io.circe.Error,List[Foo]] = Right(List(Foo(1), Foo(2), Foo(3)))

Если бы мы унаследовали неявный механизм здесь, это выглядело бы так:

scala> parser.decode[List[Foo]]("[1, 2, 3]")(Decoder.decodeList(decodeFoo))
val res1: Either[io.circe.Error,List[Foo]] = Right(List(Foo(1), Foo(2), Foo(3)))

Обратите внимание, что мы могли бы заменить первый тип деривации со вторым, и он все равно будет компилироваться:

scala> import io.circe.generic.semiauto.deriveDecoder
import io.circe.generic.semiauto.deriveDecoder

scala> parser.decode[List[Foo]]("[1, 2, 3]")(deriveDecoder[List[Foo]])
val res2: Either[io.circe.Error,List[Foo]] = Left(DecodingFailure(CNil, List()))

Это компилируется, потому что Scala s List является типом данных algebrai c, который имеет представление generi c, которое circe-generi c может создать экземпляр для. Однако декодирование не выполняется для этого ввода, поскольку это представление не приводит к ожидаемой кодировке. Мы можем получить соответствующий кодировщик, чтобы увидеть, как выглядит эта кодировка:

scala> import io.circe.Encoder, io.circe.generic.semiauto.deriveEncoder
import io.circe.Encoder
import io.circe.generic.semiauto.deriveEncoder

scala> implicit val encodeFoo: Encoder[Foo] = Encoder[Int].contramap(_.i)
val encodeFoo: io.circe.Encoder[Foo] = io.circe.Encoder$$anon$1@2717857a

scala> deriveEncoder[List[Foo]].apply(List(Foo(1), Foo(2)))
val res3: io.circe.Json =
{
  "::" : [
    1,
    2
  ]
}

Таким образом, мы на самом деле видим класс :: для List, который в принципе никогда не соответствует желаемому.

Если вам нужно явно указать Decoder[List[Foo]], решение состоит в том, чтобы использовать либо метод Decoder.apply "summoner", либо явный вызов Decoder.decodeList:

scala> Decoder[List[Foo]]
val res4: io.circe.Decoder[List[Foo]] = io.circe.Decoder$$anon$44@5d40f590

scala> Decoder.decodeList[Foo]
val res5: io.circe.Decoder[List[Foo]] = io.circe.Decoder$$anon$44@2f936a01

scala> Decoder.decodeList(decodeFoo)
val res6: io.circe.Decoder[List[Foo]] = io.circe.Decoder$$anon$44@7f525e05

. точно такой же экземпляр, и который вы должны выбрать - дело вкуса.


В качестве сноски я подумал о специальном корпусе List в circe-generi c, чтобы deriveDecoder[List[X]] не компилируется, так как это примерно никогда не то, что вы хотите (но, похоже, это может быть, особенно из-за запутанного способа, которым мы говорим о деривации экземпляра). Мне обычно не нравится идея иметь такие особые случаи, но я думаю, что в этом случае это может быть правильным решением, так как этот вопрос часто возникает.

...