Given the following ADT
sealed abstract class GroupRepository(val `type`: String) {
def name: String
def repositories: Seq[String]
def blobstore: String
}
case class DockerGroup(name: String, repositories: Seq[String], blobstore: String = "default") extends GroupRepository("docker")
case class BowerGroup(name: String, repositories: Seq[String], blobstore: String = "default") extends GroupRepository("bower")
case class MavenGroup(name: String, repositories: Seq[String], blobstore: String = "default") extends GroupRepository("maven")
Where the value type
is used to decode which instance to instantiate.
How can I automatically (or semi-automatically) derive encoders and decoders such I get the following behavior:
> println(MavenGroup("test", Seq("a", "b")).asJson.spaces2)
{
"type" : "maven",
"name" : "test",
"repositories" : [
"a",
"b"
],
"blobstore" : "default"
}
> println((MavenGroup("test", Seq("a", "b")): GroupRepository).asJson.spaces2)
{
"type" : "maven",
"name" : "test",
"repositories" : [
"a",
"b"
],
"blobstore" : "default"
}
The traditional approach of doing
object GroupRepository {
implicit val encoder = semiauto.deriveEncoder[GroupRepository]
implicit val decoder = semiauto.deriveDecoder[GroupRepository]
}
Fails on two fronts:
type
.MavenGroup("test", Seq("a", "b")).asJson
. It only allows the second alternative where MavenGroup
is first casted to GroupRepository
.The best solution I could come up with is:
object GroupRepository {
implicit def encoder[T <: GroupRepository]: Encoder[T] = Encoder.instance(a => Json.obj(
"type" -> Json.fromString(a.`type`),
"name" -> Json.fromString(a.name),
"repositories" -> Json.fromValues(a.repositories.map(Json.fromString)),
"blobstore" -> Json.fromString(a.blobstore)
))
implicit def decoder[T <: GroupRepository]: Decoder[T] = Decoder.instance(c =>
c.downField("type").as[String].flatMap {
case "docker" => c.as[DockerGroup](semiauto.deriveDecoder[DockerGroup])
case "bower" => c.as[BowerGroup](semiauto.deriveDecoder[BowerGroup])
case "maven" => c.as[MavenGroup](semiauto.deriveDecoder[MavenGroup])
}.right.map(_.asInstanceOf[T])
)
}
However it was several shortcomings:
You can define encoder/decoder (codec) for each case class as val
it will not be created each time:
import io.circe.generic.semiauto.deriveCodec
import io.circe.Codec
private implicit val dockerCodec: Codec.AsObject[DockerGroup] = deriveCodec[DockerGroup]
private implicit val bowerCodec: Codec.AsObject[BowerGroup] = deriveCodec[BowerGroup]
private implicit val mvnCodec: Codec.AsObject[MavenGroup] = deriveCodec[MavenGroup]
I think there is no way to do such a universal codec without isInstanceOf
but I can mistake.
So, I would define codec for some T
which uses concrete codec depends on type
field:
import io.circe.Decoder.Result
import io.circe.generic.semiauto.deriveCodec
import io.circe.{Codec, HCursor, Json}
import io.circe.syntax._
object Codecs {
private implicit val dockerCodec: Codec.AsObject[DockerGroup] = deriveCodec[DockerGroup]
private implicit val bowerCodec: Codec.AsObject[BowerGroup] = deriveCodec[BowerGroup]
private implicit val mvnCodec: Codec.AsObject[MavenGroup] = deriveCodec[MavenGroup]
implicit def codec[T <: GroupRepository]: Codec[T] = new Codec[T] {
override def apply(a: T): Json =
(a match {
case d: DockerGroup => d.asInstanceOf[DockerGroup].asJsonObject
case b: BowerGroup => b.asInstanceOf[BowerGroup].asJsonObject
case m: MavenGroup => m.asInstanceOf[MavenGroup].asJsonObject
}).+:("type", a.`type`.asJson).asJson
override def apply(c: HCursor): Result[T] = c.downField("type").as[String].flatMap {
case "docker" => c.as[DockerGroup]
case "bower" => c.as[BowerGroup]
case "maven" => c.as[MavenGroup]
}.map(_.asInstanceOf[T])
}
}
object Test extends App {
import Codecs._
println(DockerGroup("doc", Seq("a", "b")).asJson)
println(DockerGroup("doc", Seq("a", "b")).asJson.as[DockerGroup])
println(BowerGroup("bow", Seq("a", "b")).asJson)
println(BowerGroup("bow", Seq("a", "b")).asJson.as[BowerGroup])
println(MavenGroup("mvn", Seq("a", "b")).asJson)
println(MavenGroup("mvn", Seq("a", "b")).asJson.as[MavenGroup])
}
output:
{
"type" : "docker",
"name" : "doc",
"repositories" : [
"a",
"b"
],
"blobstore" : "default"
}
Right(DockerGroup(doc,List(a, b),default))
{
"type" : "bower",
"name" : "bow",
"repositories" : [
"a",
"b"
],
"blobstore" : "default"
}
Right(BowerGroup(bow,List(a, b),default))
{
"type" : "maven",
"name" : "mvn",
"repositories" : [
"a",
"b"
],
"blobstore" : "default"
}
Right(MavenGroup(mvn,List(a, b),default))
type
field and use configurable derivationWe can define discriminator field type
in our Configuration
for parsing JSON and get rid of this field in parent abstract class GroupRepository
(it can be a trait now).
Here I use some ad-hoc discriminator to get similar result of type
field in result json, but I think in this approach it could be more elegant:
constructorName => constructorName.toLowerCase.dropRight("Group".length)
Remember, for using ConfiguredJsonCodec
annotation you should define implicit val config: Configuration
in the companion object. Also, you should add -Ymacro-annotations
flag to the scala compiler for macroses:
scalacOptions ++= Seq("-Ymacro-annotations")
Full code:
import io.circe.Decoder.Result
import io.circe.generic.extras.{Configuration, ConfiguredJsonCodec}
import io.circe.syntax._
import io.circe.{Codec, HCursor, Json}
import ru.hardmet.GroupRepository._
@ConfiguredJsonCodec
sealed trait GroupRepository {
def name: String
def repositories: Seq[String]
def blobstore: String
}
case class DockerGroup(name: String, repositories: Seq[String], blobstore: String = "default")
extends GroupRepository
case class BowerGroup(name: String, repositories: Seq[String], blobstore: String = "default")
extends GroupRepository
case class MavenGroup(name: String, repositories: Seq[String], blobstore: String = "default")
extends GroupRepository
object GroupRepository {
implicit val config: Configuration =
Configuration.default
.withDiscriminator("type").copy(
transformConstructorNames = _.toLowerCase.dropRight("Group".length)
)
}
object GroupRepositoryCodec {
implicit def codec[T <: GroupRepository]: Codec[T] = new Codec[T] {
override def apply(a: T): Json = a.asInstanceOf[GroupRepository].asJson
override def apply(c: HCursor): Result[T] = c.as[GroupRepository].map(_.asInstanceOf[T])
}
}
object JsonExperiments extends App {
import GroupRepositoryCodec._
println(DockerGroup("doc", Seq("a", "b")).asJson)
println(DockerGroup("doc", Seq("a", "b")).asJson.as[DockerGroup])
println(BowerGroup("bow", Seq("a", "b")).asJson)
println(BowerGroup("bow", Seq("a", "b")).asJson.as[BowerGroup])
println(MavenGroup("mvn", Seq("a", "b")).asJson)
println(MavenGroup("mvn", Seq("a", "b")).asJson.as[MavenGroup])
}
the output will be the same with a difference - type
field in JSON goes to the end of object:
{
"name" : "doc",
"repositories" : [
"a",
"b"
],
"blobstore" : "default",
"type" : "docker"
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With