Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deriving circe Codec for a sealed case class family where base trait has a (sealed) type member

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?

like image 728
Giovanni Caporaletti Avatar asked Apr 25 '16 14:04

Giovanni Caporaletti


Video Answer


1 Answers

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.

like image 138
pyrospade Avatar answered Sep 22 '22 21:09

pyrospade