-
Notifications
You must be signed in to change notification settings - Fork 450
Description
Tapir version: 0.17.12
Scala version: 2.13.5
Prerequisites
So let's say I have the following error ADT:
sealed trait ErrorInfo
case class NotFound(what: String) extends ErrorInfo
case class Unauthorized(realm: String) extends ErrorInfo
case class Unknown(code: Int, msg: String) extends ErrorInfo
case object NoContent extends ErrorInfo
object ErrorInfo {
implicit val circeCodec: circe.Codec[ErrorInfo] = ???
}
And defined circe codec produces and expects the discriminator object:
NotFound(what = "something").asJson.spaces2 ==
"""
{
"not_found" : {
"what" : "something"
}
}
"""
Problem
I want to use ErrorInfo
in the error output of my endpoint, so I write this code:
val baseEndpoint = endpoint.errorOut(
oneOf[ErrorInfo](
statusMapping(StatusCode.NotFound, jsonBody[NotFound].description("not found")),
statusMapping(StatusCode.Unauthorized, jsonBody[Unauthorized].description("unauthorized")),
statusMapping(StatusCode.NoContent, emptyOutput.map(_ => NoContent)(_ => ())),
statusDefaultMapping(jsonBody[Unknown].description("unknown"))
)
)
And it does not compile, because the compiler can't find circe codec for each case of the ADT:
[error] could not find implicit value for evidence parameter of type io.circe.Encoder[NotFound]
[error] statusMapping(StatusCode.NotFound, jsonBody[NotFound].description("not found")),
which is understendable actually because of jsonBody
signature: def jsonBody[T: Encoder : Decoder: Schema]
— T here and in circe's typeclasses is invariant.
I could use import io.circe.generic.auto._
but then the output differs from what I expect: it's just plain JSON without discriminator: { "what" : "something" }
.
Workaround
For now, I am using this custom output:
def jsonBodyADT[A: Encoder: Decoder, B <: A: Schema: ClassTag]: EndpointIO.Body[String, B] = {
implicit val bEncoder: Encoder[B] = Encoder.instance[B](Encoder[A].apply(_))
implicit val bDecoder: Decoder[B] = Decoder.instance[B](
json =>
Decoder[A].apply(json) match {
case Left(value) => Left(value)
case Right(value) if value.getClass == implicitly[ClassTag[B]].runtimeClass =>
Right(value.asInstanceOf[B]) //scalafix:ok
case _ => Left(DecodingFailure.fromThrowable(new Throwable("wrong subtype"), Nil))
}
)
//and then in oneOf(...)
statusMapping(StatusCode.NotFound, jsonBodyADT[ErrorInfo, NotFound].description("not found"))
and this works but feels very wrong.
Question
So what can be done to fix this behaviour? Is there a built-in way to work with that in Tapir? If there is none, can I contribute it?