生成的播放 json 隐式实例导致 StackOverflowError

Generated play json implicit instances cause StackOverflowError

我有以下代码:

import play.api.libs.json._
object Test {
   sealed trait T
   case class A(s: String) extends T

   implicit val writesA: OWrites[A] = Json.writes[A]
   implicit val writesT: OWrites[T] = Json.writes[T]

   def main(args: Array[String]): Unit = {
      val x = A("str")
      println(Json.toJson[T](x)(writesT))
   }
}

当 运行 时会导致 WhosebugError

如果我在 writesT 前面添加 lazyWhosebugError 就会消失,一切正常:

import play.api.libs.json._
object Test {
   sealed trait T
   case class A(s: String) extends T

   implicit val writesA: OWrites[A] = Json.writes[A]
   implicit lazy val writesT: OWrites[T] = Json.writes[T]

   def main(args: Array[String]): Unit = {
      val x = A("str")
      println(Json.toJson[T](x)(writesT))
   }
}

当我将 implicit 移入 main 函数时,WhosebugError 也会消失:

import play.api.libs.json._
object Test {
   sealed trait T
   case class A(s: String) extends T

   def main(args: Array[String]): Unit = {
      implicit val writesA: OWrites[A] = Json.writes[A]
      implicit val writesT: OWrites[T] = Json.writes[T]
      val x = A("str")
      println(Json.toJson[T](x)(writesT))
   }
}

谁能给我解释一下为什么我在第一种情况下会得到 WhosebugError

我怀疑它与初始化顺序和 play-json 在后台使用的宏有关。但如果是这种情况,我不明白为什么使用 lazy 会有所帮助,因为代码仍应在编译时生成,并且稍后在 运行 时简单地对其进行评估不应该改变任何东西。显然在后面的情况下 writesA 实例被 writesT 找到,但在第一种情况下没有。为什么添加 lazy 可以解决隐含解析和宏代码生成的编译时问题?

或者这是一个完全不同层面的问题?

我正在使用 Scala 2.12.3 和 play-json 2.6.2.

这与 Play JSON 2.7.x

配合得很好
import play.api.libs.json._

sealed trait T
case class A(s: String) extends T

implicit val writesA: OWrites[A] = Json.writes[A]
implicit val writesT: OWrites[T] = Json.writes[T]

val x = A("str")
println(Json.toJson[T](x)(writesT))

// Exiting paste mode, now interpreting.

scala> println(Json.toJson[T](x)(writesT))
                                   ^
{"s":"str","_type":"$line2.$read.$iw.$iw.A"}

猜想这与 "blight of contravariant" on Writes implicit 之后的修复有关。

Play JSON 2.6 的解决方法是手动实现密封特性的 Writes(或 OFormat)实例。

scala> :paste
// Entering paste mode (ctrl-D to finish)

import play.api.libs.json._

sealed trait T
case class A(s: String) extends T

implicit val writesT: OWrites[T] = {
  val writesA: OWrites[A] = Json.writes[A]

  OWrites[T] {
    case t: A => writesA.writes(t) + ("_type" -> Json.toJson("A"))
    case _ => ???
  }
}

val x = A("str")

// Exiting paste mode, now interpreting.

import play.api.libs.json._
defined trait T
defined class A
writesT: play.api.libs.json.OWrites[T] = play.api.libs.json.OWrites$$anon@69a03da1
x: A = A(str)

scala> println(Json.toJson[T](x)(writesT))
{"s":"str","_type":"A"}