Search code examples
scalacirce

Why it does not decode to ADT type?


I am trying to derive the following string into a proper ADT type:

res6: String = {"raw":"Hello","status":{"MsgSuccess":{}}} 

and using the circe library.

The ADT type looks as the following:

sealed trait MsgDoc {
}

final case class MsgPreFailure(raw: String, reasons: Chain[String]) extends MsgDoc

final case class MsgProceed(raw: String, status: MsgStatus) extends MsgDoc

and the MsgStatus type:

sealed trait MsgStatus {

}

case object MsgSuccess extends MsgStatus

final case class MsgFailure(reasons: Chain[String]) extends MsgStatus

final case class MsgUnknown(reason: String) extends MsgStatus

and the way, I've tried to drive:

object MsgDocDerivation {

  import shapeless.{Coproduct, Generic}

  implicit def encodeAdtNoDiscr[A, Repr <: Coproduct](implicit
                                                      gen: Generic.Aux[A, Repr],
                                                      encodeRepr: Encoder[Repr]
                                                     ): Encoder[A] = encodeRepr.contramap(gen.to)

  implicit def decodeAdtNoDiscr[A, Repr <: Coproduct](implicit
                                                      gen: Generic.Aux[A, Repr],
                                                      decodeRepr: Decoder[Repr]
                                                     ): Decoder[A] = decodeRepr.map(gen.from)
}

and the execution:

object Main extends App {


  val json = MsgProceed("Hello", MsgSuccess).asJson
  println(json)
  val adt = decode[MsgDoc](json.noSpaces)
  println(adt)

} 

as the result I've got:

{
  "raw" : "Hello",
  "status" : {
    "MsgSuccess" : {

    }
  }
}
Left(DecodingFailure(CNil, List())) 

As you can see, it does not decode properly.

The source code can be find https://gitlab.com/playscala/adtjson.


Solution

  • I'm not really sure what the MsgDocDerivation stuff is intended to do—it seems unnecessary and distracting—but I think the key problem is that circe's encoding (and decoding) is driven by static types, not the runtime class of the value being encoded (or decoded). This means that the following two JSON values will be different:

    val value = MsgProceed("Hello", MsgSuccess)
    
    val json1 = value.asJson
    val json2 = (value: MsgDoc).asJson
    

    In your case the following works just fine for me:

    import cats.data.Chain
    
    sealed trait MsgStatus
    case object MsgSuccess extends MsgStatus
    final case class MsgFailure(reasons: Chain[String]) extends MsgStatus
    final case class MsgUnknown(reason: String) extends MsgStatus
    
    sealed trait MsgDoc
    final case class MsgPreFailure(raw: String, reasons: Chain[String]) extends MsgDoc
    final case class MsgProceed(raw: String, status: MsgStatus) extends MsgDoc
    
    import io.circe.generic.auto._, io.circe.jawn.decode, io.circe.syntax._
    
    val value: MsgDoc = MsgProceed("Hello", MsgSuccess)
    val json = value.asJson
    
    val backToValue = decode[MsgDoc](json.noSpaces)
    

    Note that json is different from what you were seeing:

    scala> json
    res0: io.circe.Json =
    {
      "MsgProceed" : {
        "raw" : "Hello",
        "status" : {
          "MsgSuccess" : {
    
          }
        }
      }
    }
    
    scala> backToValue
    res1: Either[io.circe.Error,MsgDoc] = Right(MsgProceed(Hello,MsgSuccess))
    

    This is because I've performed a (typesafe) upcast from MsgProceed to MsgDoc. This is typically how you work with ADTs, anyway—you're not passing around values statically typed as the case class subtypes, but rather as the sealed trait base type.