scalacodeccircetapir

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.


Solution

  • 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"
    }