I have the following code:
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))
}
}
It causes a StackOverflowError
when run.
If I add lazy
in front of writesT
, the StackOverflowError
goes away and everything works:
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))
}
}
The StackOverflowError
also disappears when I move the implicit
s into the main
function:
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))
}
}
Can anyone explain to me why I get a StackOverflowError
in the first case?
My suspicion is that it has something to do with initialization orders and the macros that play-json uses in the background. But if that is the case I don't get why using lazy
helps, because the code should still be generated at compile time and simply evaluating it later at runtime shouldn't change anything. Apparently in the later cases the writesA
instance is found by writesT
but not in the first case. Why does adding lazy
resolve a compile-time issue with the resolution of implicits and macro code generation?
Or is this an issue on a completely different level?
I am using Scala 2.12.3 and play-json 2.6.2.
This is working just fine with 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"}
Guess it's related to the "blight of contravariant" on Writes
implicit fixed since.
Workaround for Play JSON 2.6 is to implement the Writes
(or OFormat
) instance for the sealed trait manually.
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$3@69a03da1
x: A = A(str)
scala> println(Json.toJson[T](x)(writesT))
{"s":"str","_type":"A"}