The most straightforward way to do this would be to modify the derived decoders for A
and B
so that they fail when the eventType
isn't the right value. This will cause the coproduct decoder to find the appropriate case naturally:
import shapeless._
import io.circe.Decoder, io.circe.syntax._
import io.circe.generic.semiauto.deriveDecoder
import io.circe.generic.auto._, io.circe.shapes._
case class A(eventType: String, fieldOne: Int)
case class B(eventType: String, fieldOne: Int, fieldTwo: Int)
type Event = A :+: B :+: CNil
case class X(id: String, events: List[Event])
implicit val decodeA: Decoder[A] = deriveDecoder[A].emap {
case a @ A("A", _) => Right(a)
case _ => Left("Invalid eventType")
}
implicit val decodeB: Decoder[B] = deriveDecoder[B].emap {
case b @ B("B", _, _) => Right(b)
case _ => Left("Invalid eventType")
}
val doc = """{
"id" : "id",
"events" : [
{
"eventType" : "B",
"fieldOne": 1,
"fieldTwo" : 2
}
]
}"""
And then:
scala> io.circe.jawn.decode[X](doc)
res0: Either[io.circe.Error,X] = Right(X(id,List(Inr(Inl(B(B,1,2))))))
Note that you can still use automatically-derived encoders—you just need the additional check on the decoding side. (This is of course assuming you are making sure not to construct A
or B
values with invalid event types, but since you ask about using that member as a discriminator, that seems fine.)
Update: if you don't want to enumerate decoders, you could do something like this:
import io.circe.generic.decoding.DerivedDecoder
def checkType[A <: Product { def eventType: String }](a: A): Either[String, A] =
if (a.productPrefix == a.eventType) Right(a) else Left("Invalid eventType")
implicit def decodeSomeX[A <: Product { def eventType: String }](implicit
decoder: DerivedDecoder[A]
): Decoder[A] = decoder.emap(checkType)
…and it should work exactly the same as the code above. There's a small (almost certainly negligible) runtime cost for the structural type stuff, but it's perfectly safe and seems to me like a reasonable way to abstract over these types.