Tapir 无法使用“DecodingFailure(CNil, List(DownArray))”解码密封特征列表

Tapir fails to decode a list of sealed trait with `DecodingFailure(CNil, List(DownArray))`

Tapir 文档指出它支持解码密封特征:https://tapir.softwaremill.com/en/latest/endpoint/customtypes.html#sealed-traits-coproducts

但是,当我尝试使用此代码这样做时,出现以下错误:

import io.circe.generic.auto._
import sttp.client3._
import sttp.tapir.{Schema, _}
import sttp.tapir.client.sttp._
import sttp.tapir.generic.auto._
import sttp.tapir.json.circe._


object TmpApp extends App {

  sealed trait Result {
    def status: String
  }
  final case class IpInfo(
                           query: String,
                           country: String,
                           regionName: String,
                           city: String,
                           lat: Float,
                           lon: Float,
                           isp: String,
                           org: String,
                           as: String,
                           asname: String
                         ) extends Result {
    def status: String = "success"
  }
  final case class Fail(message: String, query: String) extends Result {
    def status: String = "fail"
  }

  val sIpInfo = Schema.derive[IpInfo]
  val sFail = Schema.derive[Fail]
  implicit val sResult: Schema[Result] =
    Schema.oneOfUsingField[Result, String](_.status, _.toString)("success" -> sIpInfo, "fail" -> sFail)

  val apiEndpoint = endpoint.get
    .in("batch")
    .in(query[String]("fields"))
    .in(jsonBody[List[String]])
    .out(jsonBody[List[Result]])
    .errorOut(stringBody)

  val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend()

  apiEndpoint
    .toSttpRequestUnsafe(uri"http://ip-api.com/")
    .apply(("4255449", List(
      "127.0.0.1"
    )))
    .send(backend)
    .body
}
Exception in thread "main" java.lang.IllegalArgumentException: Cannot decode from [{"status":"fail","message":"reserved range","query":"127.0.0.1"}] of request GET http://ip-api.com//batch?fields=4255449
    at sttp.tapir.client.sttp.EndpointToSttpClient.$anonfun$toSttpRequest(EndpointToSttpClient.scala:42)
    at sttp.client3.ResponseAs.$anonfun$map(ResponseAs.scala:27)
    at sttp.client3.MappedResponseAs.$anonfun$mapWithMetadata(ResponseAs.scala:89)
    at sttp.client3.MappedResponseAs.$anonfun$mapWithMetadata(ResponseAs.scala:89)
    at sttp.client3.internal.BodyFromResponseAs.$anonfun$doApply(BodyFromResponseAs.scala:23)
    at sttp.client3.monad.IdMonad$.map(IdMonad.scala:8)
    at sttp.monad.syntax$MonadErrorOps.map(MonadError.scala:42)
    at sttp.client3.internal.BodyFromResponseAs.doApply(BodyFromResponseAs.scala:23)
    at sttp.client3.internal.BodyFromResponseAs.$anonfun$apply(BodyFromResponseAs.scala:13)
    at sttp.monad.syntax$MonadErrorOps.map(MonadError.scala:42)
    at sttp.client3.internal.BodyFromResponseAs.apply(BodyFromResponseAs.scala:13)
    at sttp.client3.HttpURLConnectionBackend.readResponse(HttpURLConnectionBackend.scala:243)
    at sttp.client3.HttpURLConnectionBackend.$anonfun$send(HttpURLConnectionBackend.scala:57)
    at scala.util.Try$.apply(Try.scala:210)
    at sttp.monad.MonadError.handleError(MonadError.scala:14)
    at sttp.monad.MonadError.handleError$(MonadError.scala:13)
    at sttp.client3.monad.IdMonad$.handleError(IdMonad.scala:6)
    at sttp.client3.SttpClientException$.adjustExceptions(SttpClientException.scala:56)
    at sttp.client3.HttpURLConnectionBackend.adjustExceptions(HttpURLConnectionBackend.scala:293)
    at sttp.client3.HttpURLConnectionBackend.send(HttpURLConnectionBackend.scala:31)
    at sttp.client3.HttpURLConnectionBackend.send(HttpURLConnectionBackend.scala:23)
    at sttp.client3.FollowRedirectsBackend.sendWithCounter(FollowRedirectsBackend.scala:22)
    at sttp.client3.FollowRedirectsBackend.send(FollowRedirectsBackend.scala:17)
    at sttp.client3.RequestT.send(RequestT.scala:299)
    at onlinenslookup.ipapi.TmpApp$.delayedEndpoint$onlinenslookup$ipapi$TmpApp(TmpApp.scala:53)
    at onlinenslookup.ipapi.TmpApp$delayedInit$body.apply(TmpApp.scala:11)
    at scala.Function0.apply$mcV$sp(Function0.scala:39)
    at scala.Function0.apply$mcV$sp$(Function0.scala:39)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17)
    at scala.App.$anonfun$main(App.scala:73)
    at scala.App.$anonfun$main$adapted(App.scala:73)
    at scala.collection.IterableOnceOps.foreach(IterableOnce.scala:553)
    at scala.collection.IterableOnceOps.foreach$(IterableOnce.scala:551)
    at scala.collection.AbstractIterable.foreach(Iterable.scala:920)
    at scala.App.main(App.scala:73)
    at scala.App.main$(App.scala:71)
    at onlinenslookup.ipapi.TmpApp$.main(TmpApp.scala:11)
    at onlinenslookup.ipapi.TmpApp.main(TmpApp.scala)
Caused by: DecodingFailure(CNil, List(DownArray))

Process finished with exit code 1

build.sbt:

  "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.17.0-M10",
  "com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % "0.17.0-M10",
  "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "0.17.0-M10",

可在此处找到此特定端点的文档:https://ip-api.com/docs/api:batch

解码委托给Circe。文档中描述的只是 Schemas 的推导 - 这是文档所必需的。

因此,我会通过检查范围内是否有正确的 Decoder 来查找错误原因,并检查如果您尝试直接使用 circe 解码示例值会发生什么。

以下是我解决问题的方法,以供日后参考。

原来我少了一个CirceDecoder:

implicit val decoderResult: Decoder[Result] = Decoder[Fail].widen or Decoder[IpInfo].widen

在代码开始工作后,我也稍微清理了一下代码。

import cats.syntax.functor._
import io.circe.Decoder
import io.circe.generic.auto._
import sttp.client3._
import sttp.tapir._
import sttp.tapir.client.sttp._
import sttp.tapir.generic.auto._
import sttp.tapir.json.circe._

object TmpApp extends App {

  sealed trait Result
  final case class IpInfo(
      query: String,
      country: String,
      regionName: String,
      city: String,
      lat: Float,
      lon: Float,
      isp: String,
      org: String,
      as: String,
      asname: String
  )                                                     extends Result
  final case class Fail(message: String, query: String) extends Result

  implicit val decoderResult: Decoder[Result] = Decoder[Fail].widen or Decoder[IpInfo].widen

  val apiEndpoint =
    endpoint.get
      .in("batch")
      .in(query[String]("fields"))
      .in(jsonBody[List[String]])
      .out(jsonBody[List[Result]])
      .errorOut(stringBody)

  val backend: SttpBackend[Identity, Any] = HttpURLConnectionBackend()

  println(
    apiEndpoint
      .toSttpRequestUnsafe(uri"http://ip-api.com/")
      .apply(("4255449", List("127.0.0.1")))
      .send(backend)
      .body
  )
}