Search code examples
scalacircesttptapir

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


The Tapir documentation states that it supports decoding sealed traits: https://tapir.softwaremill.com/en/latest/endpoint/customtypes.html#sealed-traits-coproducts

However, when I try to do so using this code, I get the following error:

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$7(EndpointToSttpClient.scala:42)
    at sttp.client3.ResponseAs.$anonfun$map$1(ResponseAs.scala:27)
    at sttp.client3.MappedResponseAs.$anonfun$mapWithMetadata$1(ResponseAs.scala:89)
    at sttp.client3.MappedResponseAs.$anonfun$mapWithMetadata$1(ResponseAs.scala:89)
    at sttp.client3.internal.BodyFromResponseAs.$anonfun$doApply$2(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$1(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$1(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$1(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$1(App.scala:73)
    at scala.App.$anonfun$main$1$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",

The documentation for this specific endpoint can be found here: https://ip-api.com/docs/api:batch


Solution

  • The decoding is delegated to Circe. What is described in the documentation is only derivation of Schemas - which are necessary for documentation.

    Hence, I'd be looking for the cause of the error by checking if you have the proper Decoder in scope, and checking what happens if you try to decode an example value directly using circe.