Невозможно декодировать подклассы с помощью Circe в Scala

У меня есть проект Scala, в котором я использую Circe для обработки json. У меня проблемы с декодированием из JSON в подклассы иерархии.

Код, с которым у меня возникли проблемы, - это следующий тест:

  test("FailingResponse - Conversion between case object and Json works") {
    val caseObject = FailingResponse("Some Error", StatusCodes.INTERNAL_ERROR)
    val jsonString = caseObject
      .asJson
      .printWith(Printer.noSpaces)

    decode[ValuationResponse](jsonString) must be(Right(caseObject))
  }

Я хочу иметь возможность декодировать любой из подклассов ValuationResponse, поскольку в момент декодирования я не могу точно знать, является ли ответ FailingResponse или SuccessfulResponse. Я хотел бы, чтобы декодер мог определять, какой это тип ValuationReponse, декодировать его и делать доступным как «общий» ValuationResponse. Затем я мог бы манипулировать им, используя регистр совпадений или что-то в этом роде, чтобы получить конкретный тип.

Вместо этого в этом тесте я получаю ошибку DecodingFailure. Что я делаю неправильно?

Это код иерархии:

sealed trait ValuationResponse {
  def statusCode: StatusCode
}

case class SuccessfulResponse(values: List[StockValuation], symbol: String, function: TimeSeriesType, statusCode: StatusCode) extends ValuationResponse

case class FailingResponse(reason: String, statusCode: StatusCode) extends ValuationResponse

case class ValuationRequest(function: TimeSeriesType = TIME_SERIES_INTRADAY, symbol: String, interval: IntraDayInterval = IntraDayIntervals.MIN_5)

object derivation {

  implicit val encodeResponse: Encoder[ValuationResponse] = Encoder.instance {
    case response@SuccessfulResponse(_, _, _, _) => response.asJson
    case response@FailingResponse(_, _) => response.asJson
  }
  implicit val decodeResponse: Decoder[ValuationResponse] =
    List[Decoder[ValuationResponse]](
      Decoder[SuccessfulResponse].widen,
      Decoder[FailingResponse].widen
    ).reduceLeft(_ or _)

  implicit val encodeRequest: Encoder[ValuationRequest] = Encoder.instance {
    case response@ValuationRequest(_, _, _) => response.asJson
  }
  implicit val decodeRequest: Decoder[ValuationRequest] =
    List[Decoder[ValuationRequest]](
      Decoder[ValuationRequest].widen
    ).reduceLeft(_ or _)
}

Это перечисления, которые он использует (да, я знаю, что перечисление для кодов состояния глупо, ах):

sealed abstract class TimeSeriesType(val text: String) extends EnumEntry {}

sealed abstract class IntraDayInterval(val text: String) extends EnumEntry {}

object TimeSeriesFunctions extends Enum[TimeSeriesType] with CirceEnum[TimeSeriesType] {
  val values: immutable.IndexedSeq[TimeSeriesType] = findValues

  case object TIME_SERIES_INTRADAY extends TimeSeriesType("TIME_SERIES_INTRADAY")

  case object TIME_SERIES_DAILY extends TimeSeriesType("TIME_SERIES_DAILY")

  case object TIME_SERIES_WEEKLY extends TimeSeriesType("TIME_SERIES_WEEKLY")

  case object TIME_SERIES_MONTHLY extends TimeSeriesType("TIME_SERIES_MONTHLY")

}

object IntraDayIntervals extends Enum[IntraDayInterval] with CirceEnum[IntraDayInterval] {
  val values: immutable.IndexedSeq[IntraDayInterval] = findValues

  case object MIN_1 extends IntraDayInterval("1min")

  case object MIN_5 extends IntraDayInterval("5min")

  case object MIN_15 extends IntraDayInterval("15min")

  case object MIN_30 extends IntraDayInterval("30min")

  case object MIN_60 extends IntraDayInterval("60min")

}

object StatusCodes extends Enum[StatusCode] with CirceEnum[StatusCode] {
  val values: immutable.IndexedSeq[StatusCode] = findValues

  case object SUCCESS extends StatusCode(200)

  case object INTERNAL_ERROR extends StatusCode(500)

  case object REQUESTER_ERROR extends StatusCode(400)

}



person LeYAUable    schedule 25.03.2020    source источник


Ответы (1)


Когда я протестировал ваш код с несколькими модификациями (удалил несколько вещей, чтобы упростить компиляцию кода):

  type StatusCode = Int
  sealed trait ValuationResponse {
    def statusCode: StatusCode
  }

  case class SuccessfulResponse(succ: String, statusCode: StatusCode) extends ValuationResponse

  case class FailingResponse(reason: String, statusCode: StatusCode) extends ValuationResponse

  case class ValuationRequest(test: String)

  object derivation {

    implicit val encodeResponse: Encoder[ValuationResponse] = Encoder.instance {
      case response@SuccessfulResponse(_, _) => response.asJson
      case response@FailingResponse(_, _) => response.asJson
    }
    implicit val decodeResponse: Decoder[ValuationResponse] =
      List[Decoder[ValuationResponse]](
        Decoder[SuccessfulResponse].widen,
        Decoder[FailingResponse].widen
      ).reduceLeft(_ or _)

    implicit val encodeRequest: Encoder[ValuationRequest] = Encoder.instance {
      case response@ValuationRequest(_) => response.asJson
    }
    implicit val decodeRequest: Decoder[ValuationRequest] =
      List[Decoder[ValuationRequest]](
        Decoder[ValuationRequest].widen
      ).reduceLeft(_ or _)
  }

  val caseObject = FailingResponse("Some Error", 200)
  val jsonString = caseObject
        .asJson
        .printWith(Printer.noSpaces)

я получил

@ decode[ValuationResponse](jsonString)
res21: Either[Error, ValuationResponse] = Left(DecodingFailure(CNil, List()))

Однако, когда я импортировал имплициты из объекта

@ import derivation._
import derivation._

@ decode[ValuationResponse](jsonString)
res23: Either[Error, ValuationResponse] = Right(FailingResponse("Some Error", 200))

Дело в том, что по умолчанию Цирцея использует поле дискриминации, чтобы различать элементы типа суммы. Вы можете увидеть, во что закодировано ваше значение, если вы не импортируете объект derivation:

@ {
  val jsonString = (caseObject : ValuationResponse)
        .asJson
        .printWith(Printer.noSpaces)
  }
jsonString: String = "{\"FailingResponse\":{\"reason\":\"Some Error\",\"statusCode\":200}}"

Итак, вы использовали автоматически производные кодеки при декодировании своего класса case - если вы удалили import io.circe.generic.auto._, ваша компиляция не удалась бы, когда вы попытались декодировать вещи без импорта кодов, которые вы написали сами (import derivation._).

Чтобы избежать подобных ситуаций в будущем:

  • не импортируйте io.circe.generic.auto._ в производство на сайте использования кодеков - это позволяет получить новый кодек для класса case, который должен использовать ваш рукописный / созданный вручную кодек (что приводит к таким ошибкам)
  • предпочитаю io.circe.generic.semiauto._ вызывать производные кодеки там, где они вам нужны (вместо Decoder[A] пишите deriveDecoder[A])
  • поместите свои полуавтоматические кодеки, а также рукописные кодеки в сопутствующие объекты типов, для которых вы производите кодеки (если возможно) - это избавит от необходимости импортировать их вручную каждый раз, когда они вам понадобятся
  import io.circe.generic.semiauto._

  sealed trait ValuationResponse ...
  object ValuationResponse {
    implicit val decodeResponse: Decoder[ValuationResponse] =
      List[Decoder[ValuationResponse]](
        deriveDecoder[SuccessfulResponse].widen,
        deriveDecoder[FailingResponse].widen
      ).reduceLeft(_ or _)
  }

КСТАТИ. использование полуавто также помогает избежать других ошибок, таких как циклическая зависимость от инициализации вашего неявного в вашем коде у вас есть:

@ derivation.decodeRequest.decodeJson("test".asJson)
java.lang.NullPointerException
  ammonite.$sess.cmd7$.<clinit>(cmd7.sc:1)

но если он использовал deriveDecoder:

implicit val decodeRequest: Decoder[ValuationRequest] =
    List[Decoder[ValuationRequest]](
      deriveDecoder[ValuationRequest].widen
    ).reduceLeft(_ or _)

вы получите:

@ val decodeRequests: Decoder[ValuationRequest] =
      List[Decoder[ValuationRequest]](
        io.circe.generic.semiauto.deriveDecoder[ValuationRequest].widen
      ).reduceLeft(_ or _)
decodeRequests: Decoder[ValuationRequest] = io.circe.generic.decoding.DerivedDecoder$$anon$1@30570f04

@ decodeRequests.decodeJson("test".asJson)
res9: Decoder.Result[ValuationRequest] = Left(DecodingFailure(Attempt to decode value on failed cursor, List(DownField(test))))
person Mateusz Kubuszok    schedule 26.03.2020
comment
Что ж, после внесения тех изменений, которые вы предложили, декодирование работает. Но при кодировании я получаю: исключение или ошибка привели к прерыванию выполнения. java.lang.StackOverflowError в io.circe.syntax.package $ EncoderOps $ .asJson $ extension (package.scala: 10) в eventbus.ValuationRequest $. $ anonfun $ encodeRequest $ 1 (eventBusCases.scala: 36) строка 36: case ответ @ ValuationRequest (_, _, _) = ›response.asJson - person LeYAUable; 26.03.2020
comment
Теперь проблема решена. Мне пришлось изменить запрос на оценку на: объект ValuationRequest {неявный val encodeRequest: Encoder [ValuationRequest] = deriveEncoder [ValuationRequest] неявный val decodeRequest: Decoder [ValuationRequest] = deriveDecoder [ValuationRequest]} Очевидно, рекурсивность asJson доставляла проблемы. - person LeYAUable; 27.03.2020