Here's what I intend - let's say I have a field called medical_payments
- it can "either" be a limit if one elects or waived
{
"medical_payments":
{
"limit_value":"one_hundred"
}
}
If it's elected as a waiver then it should be:
{
"medical_payments":
{
"waived":true
}
}
So far here's what I have:
sealed trait LimitOrWaiver
case class Limit(limit_key: String) extends LimitOrWaiver
case class Waived(waived: Boolean) extends LimitOrWaiver
case class Selection(medical_payments: LimitOrWaiver)
Sample data:
Selection(medical_payments = Limit("one_hundred")).asJson
Output:
{
"medical_payments":
{
"Limit": { "limit_value":"one_hundred" } // additional object added
}
}
Similarly for Selection(medical_payments = Waived(true)).asJson
an additional Waived:{...}
is added to the Json.
I'd like it to be an either/or. What's the best way to achieve this?
The only way that I've been able to think of (not to my liking) is to use forProductN
functions per the doc and manually do all this - but it's way to cumbersome for a large Json.
You can almost accomplish this with generic derivation using the configuration in generic-extras:
sealed trait LimitOrWaiver
case class Limit(limitValue: String) extends LimitOrWaiver
case class Waived(waived: Boolean) extends LimitOrWaiver
case class Selection(medicalPayments: LimitOrWaiver)
import io.circe.generic.extras.Configuration, io.circe.generic.extras.auto._
import io.circe.syntax._
implicit val codecConfiguration: Configuration =
Configuration.default.withDiscriminator("type").withSnakeCaseMemberNames
And then:
scala> Selection(medicalPayments = Limit("one_hundred")).asJson
res0: io.circe.Json =
{
"medical_payments" : {
"limit_value" : "one_hundred",
"type" : "Limit"
}
}
(Note that I've also changed the Scala case class member names to be Scala-idiomatic camel-case, and am handling the transformation to snake-case in the configuration.)
This isn't exactly what you want, since there's that extra type
member, but circe's generic derivation only supports round-trip-able encoders / decoders, and without a discriminator of some kind—either a member like this or the extra object layer you point out in the question—it's impossible to round-trip values of arbitrary ADTs through JSON.
This might be fine—you might not care about the extra type
in your object. If you do care, you can still use derivation with a little extra work:
import io.circe.generic.extras.Configuration, io.circe.generic.extras.auto._
import io.circe.generic.extras.semiauto._
import io.circe.ObjectEncoder, io.circe.syntax._
implicit val codecConfiguration: Configuration =
Configuration.default.withDiscriminator("type").withSnakeCaseMemberNames
implicit val encodeLimitOrWaiver: ObjectEncoder[LimitOrWaiver] =
deriveEncoder[LimitOrWaiver].mapJsonObject(_.remove("type"))
And:
scala> Selection(medicalPayments = Limit("one_hundred")).asJson
res0: io.circe.Json =
{
"medical_payments" : {
"limit_value" : "one_hundred"
}
}
If you really wanted you could even make this automatic, so that type
would be removed from any ADT encoders you derive.