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。文档中描述的只是 Schema
s 的推导 - 这是文档所必需的。
因此,我会通过检查范围内是否有正确的 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
)
}
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。文档中描述的只是 Schema
s 的推导 - 这是文档所必需的。
因此,我会通过检查范围内是否有正确的 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
)
}