scalagenericscirce

How to use circe with generic case class that extends a sealed trait


I have this minimal example, I want to create encoders/decoders with circe semi-automatic derivation for the generic case class A[T]

import io.circe.{Decoder, Encoder}
import io.circe.generic.semiauto._
import io.circe.syntax._

sealed trait MyTrait
object MyTrait {
  implicit val encoder: Encoder[MyTrait] = deriveEncoder
  implicit val decoder: Decoder[MyTrait] = deriveDecoder
}

case class A[T](value: T) extends MyTrait
object A {
  implicit def encoder[T: Encoder]: Encoder[A[T]] = deriveEncoder
  implicit def decoder[T: Decoder]: Decoder[A[T]] = deriveDecoder
}

This codes does not compile and instead outputs this error

could not find Lazy implicit value of type io.circe.generic.encoding.DerivedAsObjectEncoder[A]

And the same for the decoder

What Am I doing wrong here and how can I get it working?


Solution

  • There are few issues. One is that MyTrait Encoder/Decoder will try to dispatch encodeing/decoding into codecs for the subtypes - since e thtrait is sealed all possible traits are known, so such list can be obtained by compiler.

    BUT

    While MyTrait trait does not take type parameters, its only implementation A takes. Which basically turns it into an existential type.

    val myTrait: MyTrait = A(10)
    
    myTrait match {
      case A(x) =>
        // x is of unknown type, at best you could use runtime reflection
        // but codecs are generated with compile time reflection
    }
    

    Even if you wanted to make these codecs manually, you have no way of doing it

    object Scope {
    
      def aEncoder[T: Encoder]: Encoder[A[T]] = ...
    
      val myTraitEncoder: Encoder[MyTrait] = {
        case A(value) =>
          // value is of unknown type, how to decide what Encoder[T]
          // pass into aEncoder?
      }
    }
    

    For similar reasons you couldn't manually implement Decoder.

    To be able to implement codec manually (which is kind of prerequisite to being able to generate it), you can only remove type parameters as you go into subclasses, never add them.

    sealed trait MyTrait[T]
    case class A[T](value: T) extends MyTrait[T]
    

    This would make it possible to know what kind of T is inside A since it would be passed from MyTrait, so you could write your codec manually and they would work.

    Another problem is that reliable generation of ADT's usually require some configuration, e.g. whether or not to use discimination field. And that is provided by circe-generic-extras. Once you use it, (semi)automatic derivation is possible:

    import io.circe.{Decoder, Encoder}
    import io.circe.generic.extras.Configuration
    import io.circe.generic.extras.semiauto._
    import io.circe.syntax._
    
    // can be used to tweak e.g. discriminator field name
    implicit val config: Configuration = Configuration.default
    
    sealed trait MyTrait[T]
    object MyTrait {
      implicit def encoder[T: Encoder]: Encoder[MyTrait[A]] = deriveEncoder
      implicit def decoder[T: Decoder]: Decoder[MyTrait[A]] = deriveDecoder
    }
    
    case class A[T](value: T) extends MyTrait[T]
    object A {
      implicit def encoder[T: Encoder]: Encoder[A[T]] = deriveEncoder
      implicit def decoder[T: Decoder]: Decoder[A[T]] = deriveDecoder
    }
    

    Among other solutions to the problem, if you really didn't want to use type parameter in MyTrait but have polymorphism in A would be to replace umbound, generic A with another ADT (or sum type in Scala 3), so that list of all possible implementations would be known and enumerated.