Search code examples
scalacircehttp4shttp4s-circe

Http4s circe can not decode children


I have error model like:

sealed trait HttpError {
    val msg: String
    val cause: String
  }

  final case class HttpDecodingError(cause: String) extends HttpError {
    override val msg: String = "Decoding error"
  }

  final case class HttpInternalServerError(msg: String, cause: String) extends HttpError
  case object HttpUnauthorizedError extends HttpError {
    override val msg: String = "Invalid credentials"
    override val cause: String = ""
  }
  final case class HttpBadRequestError(msg: String, cause: String) extends HttpError

in my route I generate http error type based on this model ie:

.foldM(
          {
            case error: HttpDecodingError       => BadRequest(error.asInstanceOf[HttpError])
            case error: HttpInternalServerError => InternalServerError(error.asInstanceOf[HttpError])
            case HttpUnauthorizedError          => Unauthorized(withChallenge("Invalid credentials"))
            case error: HttpBadRequestError     => BadRequest(error.asInstanceOf[HttpError])
          },
          Ok(_)
        )

but problem is that I need to add this asInstanceOf, otherwise circe does not see encoder. My encoder looks like:

implicit val encodeHttpError: Encoder[HttpError] = (error: HttpError) =>
    Json.obj(("msg", Json.fromString(error.msg)), ("cause", Json.fromString(error.cause)))

is there a way to avoid doing asInstanceOf there?


Solution

  • You can't use encoder for HttpError for it's subclasses, because Encoder is invariant (it would work if it would be covariant).

    One solution you could use is to define encoder using parametrized def instead of val:

    implicit def encodeHttpError[E <: HttpError]: Encoder[E] = (error: E) =>
      Json.obj(
        ("msg", Json.fromString(error.msg)),
        ("cause", Json.fromString(error.cause))
      )
    

    This way you'd have instance of encoder for all subtypes of HttpError as well as for HttpError.