I can easily generically derive a codec for a sealed case class family like this:
import io.circe._
import io.circe.generic.auto._
sealed trait Base
case class X(x: Int) extends Base
case class Y(y: Int) extends Base
object Test extends App {
val encoded = Encoder[Base].apply(Y(1))
val decoded = Decoder[Base].apply(encoded.hcursor)
println(decoded) // Right(Y(1))
}
However, if I add a type member to the base class I can't do it anymore, even if it's bounded by a sealed trait:
import io.circe._
import io.circe.generic.auto._
sealed trait Inner
case class I1(i: Int) extends Inner
case class I2(s: String) extends Inner
sealed trait Base { type T <: Inner }
case class X[S <: Inner](x: S) extends Base { final type T = S }
case class Y[S <: Inner](y: S) extends Base { final type T = S }
object Test extends App {
val encodedInner = Encoder[Inner].apply(I1(1))
val decodedInner = Decoder[Inner].apply(encodedInner.hcursor) // Ok
println(decodedInner) // Right(I1(1))
// Doesn't work: could not find implicit for Encoder[Base] etc
// val encoded = Encoder[Base].apply(Y(I1(1)))
// val decoded = Decoder[Base].apply(encoded.hcursor)
// println(decoded)
}
Is there a way I can achieve what I want? If not, what can I change to get something similar?
The main reason this doesn't work is because you are trying to essentially do
Encoder[Base { type T }]
without saying what type T
is. This is analogous to expecting this function to compile -
def foo[A] = implicitly[Encoder[List[A]]]
You need to explicitly refine your type.
One way to approach this is with the Aux
pattern. You can't use the typical type Aux[S] = Base { type T = S }
since that won't give you the coproduct when trying to derive the instance (the X
and Y
classes can't extend from a type alias). Instead, we could hack around it by creating another sealed trait as Aux
and have our case classes extend from that.
So long as all of your case classes extend from Base.Aux
instead of directly from Base
, you can use the following which abuses .asInstanceOf
to appease the type system.
sealed trait Inner
case class I1(i: Int) extends Inner
case class I2(s: String) extends Inner
sealed trait Base { type T <: Inner }
object Base {
sealed trait Aux[S <: Inner] extends Base { type T = S }
implicit val encoder: Encoder[Base] = {
semiauto.deriveEncoder[Base.Aux[Inner]].asInstanceOf[Encoder[Base]]
}
implicit val decoder: Decoder[Base] = {
semiauto.deriveDecoder[Base.Aux[Inner]].asInstanceOf[Decoder[Base]]
}
}
val encoded = Encoder[Base].apply(Y(I1(1)))
val decoded = Decoder[Base].apply(encoded.hcursor)
Note that a lot of this depends on how you are actually using your types. I would imagine that you wouldn't rely on calling Encoder[Base]
directly and would instead use import io.circe.syntax._
and call the .asJson
extension method. In that case, you may be able to rely on an Encoder[Base.Aux[S]]
instance which would be inferred depending on the value being encoded/decoded. Something like the following may suffice for your use case without resorting to .asInstanceOf
hacks.
implicit def encoder[S <: Inner : Encoder]: Encoder[Base.Aux[S]] = {
semiauto.deriveEncoder
}
Again, it all depends on how you are using the instances. I'm skeptical that you actually need a type member in Base
, things would be simpler if you moved it to a generic param so the deriver could figure out the coproduct for you.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With