zio / zio-json

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

ADT Serialization: Recursive types (possible) / Collection of recursive types (not deriving/compiling) #357

Open samuelsayag opened 3 years ago

samuelsayag commented 3 years ago

Disclaimer: I am new to zio-json and new to magnolia and couldn't have a convincing answer for my problem in the discord channel so I came here to describe it because it seems to me that there is really some potential problem/enhancement to do. By all means sorry if it is due to my misunderstanding :)

For the moment it is trivial and very comfortable to serialize this kind of recursive structure:

object KingDom {
  sealed trait Human
  object Human {
    final case class Subject(name: String) extends Human
    final case class King( name: String, reignOver: Human) extends Human
  }
}

With:

object KindDomApp extends App {
  import KingDom.Human
  import KingDom.Human._
  val dom: Human = King("Louis XIV", Subject("Richelieu"))
  println(dom.toJson)
}

It serialize correctly to:

{"King":{"name":"Louis XIV","reignOver":{"Subject":{"name":"Richelieu"}}}}

But we would like to give the King much more subjects with something like (reignOver field is now a collection):

object KingDom {
  sealed trait Human
  object Human {
    final case class Subject(name: String) extends Human
    final case class King(name: String, reignOver: Iterable[Human])
        extends Human

    implicit val codec = DeriveJsonCodec.gen[Human]
  }
}

We get the error(trying Seq/List isn't changing anything):

[E]      magnolia: could not find JsonCodec.Typeclass for type Iterable[zio.aerospike.sandbox.KingDom.Human]
[E]          in parameter 'reignOver' of product type zio.aerospike.sandbox.KingDom.Human.King
[E]          in coproduct type zio.aerospike.sandbox.KingDom.Human
[E]
[E]      L161:     implicit val codec = DeriveJsonCodec.gen[Human]
[E]                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^

As the derivation is made by magnolia I went digging there and there is some example of derivation of Seq type classes so it should be possible. The thing is all the instances are in the JsonEncoder/Decoder/Codec companion object (seq/iterable...) and I don't know how it is interacting with magnolia derivation for the moment.

I am ready to help and try to solve but I would need some basic direction. Maybe also a more precise diagnostic than what I have done to be sure what is wrong. Thanks for helping.

Cheers!

fsvehla commented 3 years ago

If I understand correctly, your problem was solved in the Discord room. If I’m mistaken, please reopen.

samuelsayag commented 3 years ago

Hi @fsvehla, the problem is not solved.

I tried the solution of concrete structure such as List as advised by @fommil but the problem persist. I simply happen to have a weaker case that did not need this exact type of recursion so was eventually was not blocked.

@plokhotnyuk recognized it as a bug and also opened it in jsoniter (https://github.com/plokhotnyuk/jsoniter-scala/issues/757).

He kindly sent me some info about how he solved the problem (in jsoniter) by doing this:

Hi, Samuel! It happened that types not always can be compared by the == operator. I've fixed the issue in jsoniter-scala by using full names for testing of equality of types: https://github.com/plokhotnyuk/jsoniter-scala/commit/3bda152b1456eeead4ca96628c9c9eb512183884

I thought it would be important to support this kind of case as recursive structure.

If it is considered a minor issue though it can be closed.

tbarabas commented 1 year ago

I think I reproduced the same issue with the following code, but I am getting a null reference exception from java:

case class A(
              s: String,
              a: Option[Seq[A]]
            )

object A {
  implicit lazy val decoder: JsonDecoder[A] = DeriveJsonDecoder.gen[A]
  implicit lazy val encoder: JsonEncoder[A] = DeriveJsonEncoder.gen[A]
}

object JsonSpec extends ZIOSpecDefault {
  val spec: Spec[Any, Throwable] = suite("SelectionIdHashSpec")(
    test("Find 1-1-1 in ceId test") {
      val json = "{\"s\": \"test\", \"a\":[{\"s\": \"test\"}] }"
      val obj = json.fromJson[A]
      assertTrue(obj.toString == "...")
    }
  )
}
joroKr21 commented 1 year ago

Shouldn't it be lazy val if your type is recursive?

alexander-klimov commented 6 months ago

So, what does one do with this? Just got stuck with the same problem at work - can't create derived Decoder for a recursive type.