plokhotnyuk / jsoniter-scala

Scala macros for compile-time generation of safe and ultra-fast JSON codecs + circe booster
MIT License
740 stars 99 forks source link

Traits and JsonCodecMaker #247

Open dalegaspi opened 5 years ago

dalegaspi commented 5 years ago

This is perhaps a general misunderstanding on how the JsonCodecMaker works, but i'm struggling with using the macro when one field is a trait rather than a case class. consider this example:

trait OneTypable {
  def name: String
}

case class OneType(name: String) extends OneTypable

case class ClassUseOneType(id: String, oneType: OneTypable)

when i use the macro:

implicit JsonValueCodec[ClassUseOneType] = JsonCodecMaker.make[ClassUseOneType](CodecMakerConfig(allowRecursiveTypes = true))

i get a compile time error:

No implicit 'com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec[_]' defined for 'OneTypable'.

i'm not entirely sure how to get around this or if i've hit a limitation to what the macro can do.

thank you.

plokhotnyuk commented 5 years ago

jsoniter-scala derives codecs only for sealed traits.

Generally it is done for security reasons to avoid generation of codecs for unwanted implementations (which can presents in the compilation classpath) of generic traits like Serializable, Cloneable, etc.

Also, during code generation the make macro checks that all non-abstract sub-classes of the sealed trait will have unique discriminator value for type key (as in example bellow).

Here is how it works for sealed trait:

sealed trait OneTypable {
  def name: String
}

case class OneType(name: String) extends OneTypable

case class ClassUseOneType(id: String, oneType: OneTypable)

implicit val classUseOneTypeCodec: JsonValueCodec[ClassUseOneType] = 
  JsonCodecMaker.make[ClassUseOneType](CodecMakerConfig())

val x = readFromArray("""{"id":"XXX","oneType":{"type":"OneType","name":"YYY"}}""".getBytes("UTF-8"))
val json = writeToArray(ClassUseOneType(id = "XXX", oneType = OneType(name = "YYY")))

println(x)
println(new String(json, "UTF-8"))

Another option is to use wrapping by discriminator JSON object instead of the type property:

sealed trait OneTypable {
  def name: String
}

case class OneType(name: String) extends OneTypable

case class ClassUseOneType(id: String, oneType: OneTypable)

implicit val classUseOneTypeCodec: JsonValueCodec[ClassUseOneType] = 
  JsonCodecMaker.make[ClassUseOneType](CodecMakerConfig(discriminatorFieldName = None))

val user = readFromArray("""{"id":"XXX","oneType":{"OneType":{"name":"YYY"}}}""".getBytes("UTF-8"))
val json = writeToArray(ClassUseOneType(id = "XXX", oneType = OneType(name = "YYY")))

println(user)
println(new String(json, "UTF-8"))
plokhotnyuk commented 5 years ago

BTW, if models for serialization to XML and JSON differs too much then you can consider an ability to generate a separate model tree for JSON representation from some JsonSchema.

Also, a mapping between model trees can be defined by the Chimney library to avoid most of boilerplate.

Yet another option would be writing of custom codecs or a custom macro for their generation... Here is example of custom codecs for a simple model: https://github.com/plokhotnyuk/jsoniter-scala/blob/master/jsoniter-scala-core/src/test/scala/com/github/plokhotnyuk/jsoniter_scala/core/UserAPI.scala

dalegaspi commented 5 years ago

@plokhotnyuk ..once again, i appreciate the prompt and thorough response. as you may have probably guessed...just like in my other question, i am using scalaxb to generate the case classes...and it seems that this can be addressed by making the base trait generated as sealed (i opened a ticket or hey maybe i'll attempt to make a PR for that ticket...god help us)