Q:

Tapir Custom Codec

I am stuck at a place, I am using scala, tapir and circe.
sealed abstract class S1Error extends Product with Serializable
object S1Error {
  final case class SError(error: SMError) extends S1Error
}
sealed abstract class SMError(message: String)
object SMError {
  final case class SVError(message: String) extends SMError(message)
}

For tapir errorOut I am using this
val schemaVersionError: EndpointOutput.StatusMapping[SError] = statusMappingValueMatcher(
      StatusCode.BadRequest,
      jsonBody[SError]
        .description("XXXX.")
    ) {
      case SMError(SVError(_)) => true
      case _                                  => false
    }

Now because of this structure, the API result I get is 
{
    "error": {
        "SVError": {
            "message": "XXXXG"
        }
    }
}

where as ideally I would want a response as
"message": "XXXXG"

I can not change the error structure.
Is there a way to wrap this error using a custom codec to get the result as required.

A:

Tapir codec is derived from Circe's decoder and encoder.
What you see is the default way of encoding case classes by circe.
Circe provides the possibility to encode case classes the way you described with deriveUnwrappedEncoder from circe-generic-extras. Unfortunately, it doesn't compile for SMError (probably derivation mechanism gets confused by your abstract class hierarchy).
What you can do is just creating encoder manually:
sealed abstract class S1Error extends Product with Serializable

object S1Error {
  final case class SError(error: SMError) extends S1Error

  implicit val encoder: Encoder[SError] = Encoder[SMError].contramap(_.error)
  // or you can use deriveUnwrappedEncoder from circe-generic-extras:
  // implicit val encoder: Encoder[SError] = deriveUnwrappedEncoder
}

//I also needed to make message a field in SMError
sealed abstract class SMError(val message: String)
object SMError {
  final case class SVError(override val message: String) extends SMError(message)

  implicit val encoder: Encoder[SMError] = Encoder.encodeJsonObject.contramap{s => JsonObject("message" -> s.message.asJson)}
}

Response now looks like:
{
    message": "XXXXG"
}

