Error handling using oneOf and a single JsonEncoder

I’m trying to use the errorOut(oneOf pattern in order to specialize the status code of an endpoint based on an error. My setup is pretty similar to tapir/oneof.md at 855f2d923c8e2d10a0d4af5e49fa33574b529b22 · softwaremill/tapir · GitHub except that I’m using Scala 3’s enum and would like to keep a single json encoder/decoder for the whole ADT:

enum ServerError:
  case NotFound(msg: String, entity: String)
  case Wrapped(msg: String, underlying: String)

object ServerError:
  given decoder: JsonDecoder[ServerError] = DeriveJsonDecoder.gen
  given encoder: JsonEncoder[ServerError] = DeriveJsonEncoder.gen

When I try to define my error output as such:

    .errorOut(
      oneOf[ServerError](
        oneOfVariant(statusCode(StatusCode.NotFound).and(jsonBody[NotFound])),
        oneOfVariant(statusCode(StatusCode.InternalServerError).and(jsonBody[Wrapped]))
      )
    )

It can’t find the json encoder for NotFound and Wrapped, which is expected.

How can I used the encoder from ServerError in this case ? I think that a solution could be to use oneOfVariantValueMatcher with jsonBody[ServerError] but to match on a specific type each time such as that

      oneOf[ServerError](
        oneOfVariantValueMatcher(StatusCode.NotFound, jsonBody[ServerError]) { case _: NotFound =>
          true
        },
        oneOfVariantValueMatcher(StatusCode.InternalServerError, jsonBody[ServerError]) { case _: Wrapped =>
          true
        }
      )

That could be extracted into something like that

  def customVariantMatcher[T <: ServerError: ClassTag](code: StatusCode) =
    oneOfVariantValueMatcher(code, jsonBody[ServerError]) { case _: T => true }

But it feels pretty verbose and I’m sure that there must be another solution

I think the root cause of the problem is that JsonEncoder is invariant. So the error comes from jsonBody, not from oneOf - hence it’s in the json body’s definition, that you’ll need to provide a custom encoders/decoders. oneOfVariantValueMatcher is only concerned with picking the right oneOf branch, not with encoding/decoding itself, which is handled by the wrapped endpoint input/output.

There are two possible solutions:

  1. provide encoders/decoders for the enum members. Each encoder will be a simple .contramap, the decoder will have to be derived individually
  2. don’t use oneOf, instead use the following:
jsonBody[ServerError].and(statusCode)
  .map { case (err, statusCode) => err } {
    case e: NotFound => (e, StatusCode.NotFound)
    case e: Wrapped => (e, StatusCode.InternalServerError)
  }

That is, you create a composite output, and then map over it to flatten it to just a ServerError

In fact, I replied too fast. The second solution I proposed would work, but it wouldn’t give you the documentation you probably want - where each variant is documented with the proper status code and the proper error subtype.

For this to work, you’ll have to go with 1., and provide encoders/decoders for the individual cases.

Yes I was going to say that the documentation was not correct using 2.
I will give 1. a try today, thanks for the pointer, and this is indeed not a tapir issue in oneOf but related to the invariance of JsonEncoder/Decoder