julienrf / play-json-derived-codecs

MIT License
191 stars 34 forks source link

Include discriminator when serializing concrete type #96

Open sam-veezoo opened 3 months ago

sam-veezoo commented 3 months ago

Is there a way to include the discriminator even when serializing the concrete type? Concretely, I'd like to instantiate the library in this piece of code and get the same output in both println statements:

sealed trait Base
case class Foo(name: String) extends Base

object Test {
  def test(): Unit = {
    val foo = Foo("Hello")
    val fooAsBase: Base = foo

    println(Json.toJson(fooAsBase).toString())    // expected: { "type": "Foo", "name": "Hello" }
    println(Json.toJson(foo).toString())          // expected: { "type": "Foo", "name": "Hello" }
  }
}

The ideomatic way to instantiate the library would only provide us with Writes[Base] used in the first println statement. I attempted to use generics in order to obtain suitable implicits for Writes[Foo] and Writes[Base] as follows:

object Base {
  implicit def writes[T <: Base]: Writes[T] =
    julienrf.json.derived.flat.owrites[Base](
      (__ \ "type").write[String]
    ).asInstanceOf[Writes[T]]
}

This used to work in play-json-derived-codes version 7.0.0 (together with play-json_2.12:2.8.2), however I end up with infinite recursion when using a more recent play-json-derived-codes version 10.1.0 (together with play-json_2.13:2.10.5):

at julienrf.json.derived.DerivedOWritesUtil$$anon$6.$anonfun$owrites$6(DerivedOWrites.scala:130)
at play.api.libs.json.OWrites$$anon$4.writes(Writes.scala:150)
at play.api.libs.json.OWrites.$anonfun$contramap$2(Writes.scala:76)
at play.api.libs.json.OWrites$$anon$4.writes(Writes.scala:150)
at play.api.libs.json.OWrites$$anon$4.writes(Writes.scala:149)
at julienrf.json.derived.TypeTagOWrites$$anon$2.$anonfun$owrites$2(typetags.scala:91)
at play.api.libs.json.OWrites$$anon$4.writes(Writes.scala:150)
at julienrf.json.derived.DerivedOWritesUtil$$anon$6.$anonfun$owrites$6(DerivedOWrites.scala:130)
...

Given my attempt is quite acrobatic, I am not sure whether the infinite recursion is actually a bug in the library.

Is there a way to achieve the expected result and obtain the same JSON serialization for the sealed trait as well as for the concrete case class?

julienrf commented 3 months ago

I think the infinite recursion comes from the fact that when deriving the Writes[Base] we try to find if there is an existing Writes[Foo] to use it, but that one is the very Writes[Base] that is being derived.

I am not sure there is a better solution than requiring to upcast foo into Base:

- println(Json.toJson(foo))
+ println(Json.toJson(foo: Base))