Search code examples
jsonscalatemplatesencodingcirce

Encoding class containing instances of type parametrized class


I can not deal with usage of circe to encode instance of FL class to JSON. I provided two custom encoders - both for CS and RS classes. However, still during compilation I got:

could not find implicit value for parameter encoder: io.circe.Encoder[FL]

when I try to compile:

import io.circe.{Encoder, Json}
import io.circe.generic.extras.auto._
import io.circe.syntax._
FL(List.empty).asJson

    
implicit val rsEncoder: Encoder[RS] = Encoder.instance[RS] { x =>
    Json.obj(
    )
}

implicit val csEncoder: Encoder[CS] = Encoder.instance[CS] { x =>
    Json.obj(
    )
}


sealed trait A {
    def s: Int
}

object A {
    case class RS(s: Int, d: Double) extends A
    case class CS(s: Int, l: Double) extends A
}

case class FSI[T <: A](sts: T)

final case class E(
    segs: Seq[FSI[_ <: A]] = Seq.empty,
)

final case class FL(es: List[List[E]])

The only way I see is trial of implementation of encoder at FL class level - however I would like to avoid it as this class contain more elements (however they are not problematin in terms of encoding). Could you help me to do/workaround it? Or maybe there exist theoretical reason it is not possible.


Solution

  • I played around with your code and I think there are two things that are throwing off Circe:

    1. for this to work, it looks like you need to add a configuration that specifies a discriminator (which makes sense, since from the structure alone it's not possible to go back to which subtype of A you're encoding
    2. it doesn't look like it's possible to encode a Seq[_ <: A] (not automatically at least), so you need to just go for Seq[A] (which I don't think should limit you in practice, but happy to stand corrected)

    The following seems to work as expected:

    import io.circe.{Encoder, Json}
    import io.circe.generic.extras.auto._
    import io.circe.syntax._
    import io.circe.generic.extras.Configuration
    
    implicit val genDevConfig: Configuration =
      Configuration.default.withDiscriminator("type")
    
    sealed trait A {
        def s: Int
    }
    
    object A {
        case class RS(s: Int, d: Double) extends A
        case class CS(s: Int, l: Double) extends A
    }
    
    case class FSI[T <: A](sts: T)
    
    final case class E(
        segs: Seq[FSI[A]] = Seq.empty,
    )
    
    final case class FL(es: List[List[E]])
    
    FL(List.empty).asJson.noSpaces
    FL(List(List(E(List(FSI(A.RS(1, 4.2))))))).asJson.noSpaces
    

    Notice that I didn't even have to include the custom encoders for RS and CS.

    You can play around with this code here on Scastie (I configured to use Scala 2.13 and Circe 0.14.3, which based on your original post it seems like going in the direction of your current dependencies).

    If you want to use the custom encoders, you have to manually dispatch to each type by explicitly having an Encoder[A] as in the following snippet:

    import io.circe.{Encoder, Json}
    import io.circe.generic.extras.auto._
    import io.circe.generic.extras.Configuration
    import io.circe.syntax._
    
    implicit val config: Configuration = Configuration.default
    
    implicit val rsEncoder: Encoder[A.RS] = Encoder.instance[A.RS] { x =>
      Json.obj(
      )
    }
    
    implicit val csEncoder: Encoder[A.CS] = Encoder.instance[A.CS] { x =>
      Json.obj(
      )
    }
    
    implicit val encodeA: Encoder[A] = Encoder.instance[A] {
      case rs: A.RS => rs.asJson
      case cs: A.CS => cs.asJson
    }
    
    sealed trait A {
      def s: Int
    }
    
    object A {
      case class RS(s: Int, d: Double) extends A
      case class CS(s: Int, l: Double) extends A
    }
    
    case class FSI[T <: A](sts: T)
    
    final case class E(
        segs: Seq[FSI[A]] = Seq.empty
    )
    
    final case class FL(es: List[List[E]])
    
    FL(List.empty).asJson.noSpaces
    FL(List(List(E(List(FSI(A.RS(1, 4.2))))))).asJson.noSpaces
    

    This example is also available on Scastie. Note that in this case you'll still have to use some form of discriminator if you need to parse those JSON objects back in the same types.

    I wrote this using information from the Circe documentation about ADT encoding.