zio / zio-json

Fast, secure JSON library with tight ZIO integration.
https://zio.dev/zio-json
Apache License 2.0
407 stars 146 forks source link

Optional type discriminator #286

Open nryanov opened 3 years ago

nryanov commented 3 years ago

Hello!

I have a domain model (for consul api) and want to encode/decode types like Check and ServiceCheck and their subtypes . The issue is in model encoding/decoding results. Classes :

sealed trait Check
final case class ScriptCheck(...) extends Check
final case class HttpCheck(...) extends Check
final case class TCPCheck(...) extends Check
final case class TTLCheck(...) extends Check
final case class DockerCheck(...) extends Check
final case class GRpcCheck(...) extends Check
final case class AliasCheck(...) extends Check

and encoders/decoders:

implicit val checkEncoder: JsonEncoder[Check] = DeriveJsonEncoder.gen[Check]
implicit val checkDecoder: JsonDecoder[Check] = DeriveJsonDecoder.gen[Check]

When i encode instance of any subtype of Check or ServiceCheck i get a json object like this:

{ 
    "ScriptCheck": {...}
}

The api does not recognise field ScriptCheck and fails with an error. If i try to encode ScriptCheck explicitly using encoder for ScriptCheck type then json will not have type discriminator.

I tried to use annotations with empty values @jsonHint("") and @jsonDiscriminator(""), as i thought that it will remove it because in macros there is a code like this:

def discrim = ctx.annotations.collectFirst { case jsonDiscriminator(n) => n }
    if (discrim.isEmpty) {...} else {...}

but got an error:

Caused by: java.lang.IllegalArgumentException: requirement failed
    at scala.Predef$.require(Predef.scala:325)
    at zio.json.internal.StringMatrix.<init>(lexer.scala:393)
    at zio.json.DeriveJsonDecoder$$anon$4.<init>(macros.scala:251)
    at zio.json.DeriveJsonDecoder$.dispatch(macros.scala:249)

Also, i tried to construct encoder using eitherWith, but in result i get a json with additional field Left or Right. In the meantime, decoder can be constructed using orElse and it will not require type discriminator.

Is it possible to completely remove type discriminator?

murrelljenna commented 2 years ago

Hi! I had the same problem of this and ended up doing this:

val scriptCheckEncoder = DeriveJsonEncoder.Gen[ScriptCheck]
// ... repeat for all your types

implicit val checkEncoder: JsonEncoder[Check] = DeriveJsonEncoder.gen[Check].contramap(¨
  {
    case c:ScriptCheck => c
    // Repeat for all your types
  }
)

This seems silly, but what this is essentially doing is telling the type system to look for your implicitly defined case class encoders and use those instead of the ADT encoder.