Try to define custom codecs for the trait (without discriminator) and lazy codecs for the recursive type
import play.api.libs.json._
import play.api.libs.functional.syntax._
sealed trait Thing
object Thing {
implicit val thingReads: Reads[Thing] = Foo.fooReads.or[Thing](Bar.barReads.widen)
implicit val thingWrites: OWrites[Thing] = {
case x: Foo => Foo.fooWrites.writes(x)
case x: Bar => Bar.barWrites.writes(x)
}
}
case class Foo(i: Int) extends Thing
object Foo {
implicit val fooReads: Reads[Foo] = Json.reads[Foo]
implicit val fooWrites: OWrites[Foo] = Json.writes[Foo]
}
case class Bar(s: String, t: List[Thing]) extends Thing
object Bar {
implicit val barReads: Reads[Bar] = (
(__ \ "s").read[String] and
(__ \ "t").lazyRead(Reads.list[Thing](Thing.thingReads))
)(Bar.apply _)
implicit val barWrites: OWrites[Bar] = (
(__ \ "s").write[String] and
(__ \ "t").lazyWrite(Writes.list[Thing](Thing.thingWrites))
)(unlift(Bar.unapply))
}
val thing: Thing = Bar("doomy doomy doom", List(Foo(24), Bar("doooom!", List(Foo(1), Foo(2), Foo(3))), Foo(42), Foo(126)))
val str = Json.stringify(Json.toJson(thing))
//{"s":"doomy doomy doom","t":[{"i":24},{"s":"doooom!","t":[{"i":1},{"i":2},{"i":3}]},{"i":42},{"i":126}]}
val thing1 = Json.parse(str).as[Thing]
// Bar(doomy doomy doom,List(Foo(24), Bar(doooom!,List(Foo(1), Foo(2), Foo(3))), Foo(42), Foo(126)))
thing1 == thing // true
Scala play json nested cyclic dependency json parsing