Skip to content

Support for error ADT and circe Codec in output #1043

@FunFunFine

Description

@FunFunFine

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions