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

1.6k views Asked by At

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

2

There are 2 answers

1
adamw On BEST ANSWER

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.

0
Ruurtjan Pul On

For future reference, here's how I solved the issue.

It turned out that I was missing a Circe Decoder:

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

I've also cleaned up the code a bit after getting it to work.

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
  )
}