4
votes

I have a the following set up

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])

And I receive the following Json message, an X with one event (instance of B)

{
"id" : "id",
"events" : [
    {
      "eventType" : "B",
      "fieldOne": 1,
      "fieldTwo" : 2
    }
]
}

If I use circe I can decode this into an instance of X, however in the events list because A arrives first in the coproduct it will decode it into an A.

val actual = X("id", [A("B", 1)])
val expected = X("id", [B("B", 1, 2)])

I want to be able to use the eventType as the Configuration discriminator to determine which type in the Coproduct the nested field becomes.

I think the answer lies in here

Generic derivation for ADTs in Scala with a custom representation

but I can't quite seem to work it out for my case.

1
While the coproduct approach is cool, why not use the tag approach? Seems like it would suit your requirement perfectly and you don't need to use any complex implicit machinery few could maintain.flavian
We need the coproducts to create avro schemas elsewhereJohn Cragg

1 Answers

2
votes

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.