0

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

Ruurtjan Pul
  • 1,197
  • 1
  • 10
  • 21

2 Answers2

1

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.

adamw
  • 8,038
  • 4
  • 28
  • 32
  • Thanks Adam! I was able to reproduce the issue without Tapir, and with just Circe. I was indeed missing an implicit `Decoder`. Adding this line solved my issue: `implicit val decoderResult: Decoder[Result] = Decoder[Fail].widen or Decoder[IpInfo].widen` – Ruurtjan Pul Dec 11 '20 at 13:51
0

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
  )
}
Ruurtjan Pul
  • 1,197
  • 1
  • 10
  • 21