From 6790ec874c3ed335426af17e3cb57689d6fe7161 Mon Sep 17 00:00:00 2001 From: xuwei-k <6b656e6a69@gmail.com> Date: Fri, 24 Apr 2026 21:16:48 +0900 Subject: [PATCH 1/2] Add recursive methods --- .../play/api/libs/json/RecursiveFormat.scala | 9 ++ .../play/api/libs/json/RecursiveOFormat.scala | 9 ++ .../play/api/libs/json/RecursiveOWrites.scala | 9 ++ .../play/api/libs/json/RecursiveReads.scala | 9 ++ .../play/api/libs/json/RecursiveWrites.scala | 9 ++ .../play/api/libs/json/RecursiveFormat.scala | 35 ++++++ .../play/api/libs/json/RecursiveOFormat.scala | 35 ++++++ .../play/api/libs/json/RecursiveOWrites.scala | 32 +++++ .../play/api/libs/json/RecursiveReads.scala | 32 +++++ .../play/api/libs/json/RecursiveWrites.scala | 32 +++++ .../scala/play/api/libs/json/Format.scala | 4 +- .../main/scala/play/api/libs/json/Reads.scala | 3 +- .../scala/play/api/libs/json/Writes.scala | 4 +- .../play/api/libs/json/RecursiveSpec.scala | 119 ++++++++++++++++++ 14 files changed, 336 insertions(+), 5 deletions(-) create mode 100644 play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveFormat.scala create mode 100644 play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveOFormat.scala create mode 100644 play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveOWrites.scala create mode 100644 play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveReads.scala create mode 100644 play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveWrites.scala create mode 100644 play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveFormat.scala create mode 100644 play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOFormat.scala create mode 100644 play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOWrites.scala create mode 100644 play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveReads.scala create mode 100644 play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveWrites.scala create mode 100644 play-json/shared/src/test/scala-3/play/api/libs/json/RecursiveSpec.scala diff --git a/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveFormat.scala b/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveFormat.scala new file mode 100644 index 000000000..80089c1a2 --- /dev/null +++ b/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveFormat.scala @@ -0,0 +1,9 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +trait RecursiveFormat { self: Format.type => + +} diff --git a/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveOFormat.scala b/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveOFormat.scala new file mode 100644 index 000000000..68b417555 --- /dev/null +++ b/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveOFormat.scala @@ -0,0 +1,9 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +trait RecursiveOFormat { self: OFormat.type => + +} diff --git a/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveOWrites.scala b/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveOWrites.scala new file mode 100644 index 000000000..dc359576b --- /dev/null +++ b/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveOWrites.scala @@ -0,0 +1,9 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +trait RecursiveOWrites { self: OWrites.type => + +} diff --git a/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveReads.scala b/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveReads.scala new file mode 100644 index 000000000..77bbdde94 --- /dev/null +++ b/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveReads.scala @@ -0,0 +1,9 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +trait RecursiveReads { self: Reads.type => + +} diff --git a/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveWrites.scala b/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveWrites.scala new file mode 100644 index 000000000..6c7303edc --- /dev/null +++ b/play-json/shared/src/main/scala-2/play/api/libs/json/RecursiveWrites.scala @@ -0,0 +1,9 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +trait RecursiveWrites { self: Writes.type => + +} diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveFormat.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveFormat.scala new file mode 100644 index 000000000..155d28b53 --- /dev/null +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveFormat.scala @@ -0,0 +1,35 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import scala.annotation.tailrec + +trait RecursiveFormat { self: Format.type => + final def recursive[A](f: Format[A] ?=> Format[A]): Format[A] = { + lazy val res: Format[A] = f(using RecursiveFormat.DeferredFormat(() => res)) + res + } +} + +private[json] object RecursiveFormat { + private final case class DeferredFormat[A](value: () => Format[A]) extends Format[A] { + private lazy val resolved: Format[A] = resolve(value) + + @tailrec + private def resolve(f: () => Format[A]): Format[A] = + f() match { + case DeferredFormat(f) => + resolve(f) + case next => + next + } + + override def reads(json: JsValue): JsResult[A] = + resolved.reads(json) + + override def writes(o: A): JsValue = + resolved.writes(o) + } +} diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOFormat.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOFormat.scala new file mode 100644 index 000000000..08c792e58 --- /dev/null +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOFormat.scala @@ -0,0 +1,35 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import scala.annotation.tailrec + +trait RecursiveOFormat { self: OFormat.type => + final def recursive[A](f: OFormat[A] ?=> OFormat[A]): OFormat[A] = { + lazy val res: OFormat[A] = f(using RecursiveOFormat.DeferredOFormat(() => res)) + res + } +} + +private[json] object RecursiveOFormat { + private final case class DeferredOFormat[A](value: () => OFormat[A]) extends OFormat[A] { + private lazy val resolved: OFormat[A] = resolve(value) + + @tailrec + private def resolve(f: () => OFormat[A]): OFormat[A] = + f() match { + case DeferredOFormat(f) => + resolve(f) + case next => + next + } + + override def reads(json: JsValue): JsResult[A] = + resolved.reads(json) + + override def writes(o: A): JsObject = + resolved.writes(o) + } +} diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOWrites.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOWrites.scala new file mode 100644 index 000000000..b096295c6 --- /dev/null +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOWrites.scala @@ -0,0 +1,32 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import scala.annotation.tailrec + +trait RecursiveOWrites { self: OWrites.type => + final def recursive[A](f: OWrites[A] ?=> OWrites[A]): OWrites[A] = { + lazy val res: OWrites[A] = f(using RecursiveOWrites.DeferredOWrites(() => res)) + res + } +} + +private[json] object RecursiveOWrites { + private final case class DeferredOWrites[A](value: () => OWrites[A]) extends OWrites[A] { + private lazy val resolved: OWrites[A] = resolve(value) + + @tailrec + private def resolve(f: () => OWrites[A]): OWrites[A] = + f() match { + case DeferredOWrites(f) => + resolve(f) + case next => + next + } + + override def writes(o: A): JsObject = + resolved.writes(o) + } +} diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveReads.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveReads.scala new file mode 100644 index 000000000..7f07dbece --- /dev/null +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveReads.scala @@ -0,0 +1,32 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import scala.annotation.tailrec + +trait RecursiveReads { self: Reads.type => + final def recursive[A](f: Reads[A] ?=> Reads[A]): Reads[A] = { + lazy val res: Reads[A] = f(using RecursiveReads.DeferredReads(() => res)) + res + } +} + +private[json] object RecursiveReads { + private final case class DeferredReads[A](value: () => Reads[A]) extends Reads[A] { + private lazy val resolved: Reads[A] = resolve(value) + + @tailrec + private def resolve(f: () => Reads[A]): Reads[A] = + f() match { + case DeferredReads(f) => + resolve(f) + case next => + next + } + + override def reads(json: JsValue): JsResult[A] = + resolved.reads(json) + } +} diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveWrites.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveWrites.scala new file mode 100644 index 000000000..745fd05dd --- /dev/null +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveWrites.scala @@ -0,0 +1,32 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import scala.annotation.tailrec + +trait RecursiveWrites { self: Writes.type => + final def recursive[A](f: Writes[A] ?=> Writes[A]): Writes[A] = { + lazy val res: Writes[A] = f(using RecursiveWrites.DeferredWrites(() => res)) + res + } +} + +private[json] object RecursiveWrites { + private final case class DeferredWrites[A](value: () => Writes[A]) extends Writes[A] { + private lazy val resolved: Writes[A] = resolve(value) + + @tailrec + private def resolve(f: () => Writes[A]): Writes[A] = + f() match { + case DeferredWrites(f) => + resolve(f) + case next => + next + } + + override def writes(o: A): JsValue = + resolved.writes(o) + } +} diff --git a/play-json/shared/src/main/scala/play/api/libs/json/Format.scala b/play-json/shared/src/main/scala/play/api/libs/json/Format.scala index d077beb42..ee76f851b 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/Format.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/Format.scala @@ -29,7 +29,7 @@ trait OFormat[A] extends OWrites[A] with Reads[A] with Format[A] { } -object OFormat { +object OFormat extends RecursiveOFormat { implicit def functionalCanBuildFormats(implicit rcb: FunctionalCanBuild[Reads], wcb: FunctionalCanBuild[OWrites] @@ -64,7 +64,7 @@ object OFormat { /** * Default Json formatters. */ -object Format extends PathFormat with ConstraintFormat with DefaultFormat { +object Format extends PathFormat with ConstraintFormat with DefaultFormat with RecursiveFormat { val constraints: ConstraintFormat = this val path: PathFormat = this diff --git a/play-json/shared/src/main/scala/play/api/libs/json/Reads.scala b/play-json/shared/src/main/scala/play/api/libs/json/Reads.scala index 7a216a982..5d6313aec 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/Reads.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/Reads.scala @@ -166,7 +166,8 @@ trait Reads[A] { self => /** * Default deserializer type classes. */ -object Reads extends ConstraintReads with PathReads with DefaultReads with GeneratedReads { +object Reads extends ConstraintReads with PathReads with DefaultReads with GeneratedReads with RecursiveReads { + val constraints: ConstraintReads = this val path: PathReads = this diff --git a/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala b/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala index 7deadf4f5..e33c8cef1 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/Writes.scala @@ -78,7 +78,7 @@ trait OWrites[A] extends Writes[A] { override def narrow[B <: A]: OWrites[B] = this.asInstanceOf[OWrites[B]] } -object OWrites extends PathWrites with ConstraintWrites { +object OWrites extends PathWrites with ConstraintWrites with RecursiveOWrites { import play.api.libs.functional._ def of[A](implicit w: OWrites[A]): OWrites[A] = w @@ -237,7 +237,7 @@ object OWrites extends PathWrites with ConstraintWrites { /** * Default Serializers. */ -object Writes extends PathWrites with ConstraintWrites with DefaultWrites with GeneratedWrites { +object Writes extends PathWrites with ConstraintWrites with DefaultWrites with GeneratedWrites with RecursiveWrites { val constraints: ConstraintWrites = this val path: PathWrites = this diff --git a/play-json/shared/src/test/scala-3/play/api/libs/json/RecursiveSpec.scala b/play-json/shared/src/test/scala-3/play/api/libs/json/RecursiveSpec.scala new file mode 100644 index 000000000..2e9890d40 --- /dev/null +++ b/play-json/shared/src/test/scala-3/play/api/libs/json/RecursiveSpec.scala @@ -0,0 +1,119 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import org.scalatest.wordspec.AnyWordSpec +import play.api.libs.json.RecursiveSpec.Foo +import play.api.libs.functional.syntax.* + +final class RecursiveSpec extends AnyWordSpec { + "recursive" should { + "Reads" in { + given Reads[Foo] = Reads.recursive( + ( + (__ \ "x").read[Int] and + (__ \ "y").read[List[Foo]] + )(Foo.apply) + ) + assert(Foo.json1.as[Foo] == Foo.value1) + } + "Writes" in { + given Writes[Foo] = Writes.recursive( + ( + (__ \ "x").write[Int] and + (__ \ "y").write[List[Foo]] + )(Tuple.fromProductTyped(_)) + ) + assert(Json.toJson(Foo.value1) == Foo.json1) + } + "OWrites" in { + given OWrites[Foo] = OWrites.recursive( + ( + (__ \ "x").write[Int] and + (__ \ "y").write[List[Foo]] + )(Tuple.fromProductTyped(_)) + ) + assert(Json.toJson(Foo.value1) == Foo.json1) + } + "Format" in { + given Format[Foo] = Format.recursive( + ( + (__ \ "x").format[Int] and + (__ \ "y").format[List[Foo]] + )(Foo.apply, Tuple.fromProductTyped) + ) + assert(Json.toJson(Foo.value1) == Foo.json1) + assert(Foo.json1.as[Foo] == Foo.value1) + } + "OFormat" in { + given OFormat[Foo] = OFormat.recursive( + ( + (__ \ "x").format[Int] and + (__ \ "y").format[List[Foo]] + )(Foo.apply, Tuple.fromProductTyped) + ) + assert(Json.toJson(Foo.value1) == Foo.json1) + assert(Foo.json1.as[Foo] == Foo.value1) + } + } +} + +object RecursiveSpec { + private final case class Foo(x: Int, y: List[Foo]) + + private object Foo { + val value1: Foo = Foo( + 1, + List( + Foo(2, Nil), + Foo(3, List(Foo(4, Nil))), + Foo(5, Nil), + Foo( + 6, + List( + Foo(7, Nil), + Foo(8, Nil) + ) + ), + ) + ) + + val json1: JsObject = Json.obj( + "x" -> 1, + "y" -> Json.arr( + Json.obj( + "x" -> 2, + "y" -> Json.arr() + ), + Json.obj( + "x" -> 3, + "y" -> Json.arr( + Json.obj( + "x" -> 4, + "y" -> Json.arr(), + ) + ) + ), + Json.obj( + "x" -> 5, + "y" -> Json.arr() + ), + Json.obj( + "x" -> 6, + "y" -> Json.arr( + Json.obj( + "x" -> 7, + "y" -> Json.arr(), + ), + Json.obj( + "x" -> 8, + "y" -> Json.arr(), + ), + ) + ) + ) + ) + } +} From 450d07dc925663ea4c6c0914098a856bd38f0aa2 Mon Sep 17 00:00:00 2001 From: Matthias Kurz Date: Wed, 15 Jul 2026 13:23:39 +0200 Subject: [PATCH 2/2] Document Scala 3 recursive codecs Describe the deferred contextual codec exposed by each recursive method and clarify that these methods are available only in Scala 3. Add a guide example for recursive givens while retaining the existing Scala 2 guidance. --- .../main/json/ScalaJsonCombinators.md | 23 +++++++++++++++++++ .../play/api/libs/json/RecursiveFormat.scala | 13 +++++++++++ .../play/api/libs/json/RecursiveOFormat.scala | 13 +++++++++++ .../play/api/libs/json/RecursiveOWrites.scala | 13 +++++++++++ .../play/api/libs/json/RecursiveReads.scala | 13 +++++++++++ .../play/api/libs/json/RecursiveWrites.scala | 13 +++++++++++ 6 files changed, 88 insertions(+) diff --git a/docs/manual/working/scalaGuide/main/json/ScalaJsonCombinators.md b/docs/manual/working/scalaGuide/main/json/ScalaJsonCombinators.md index c0faed9dd..6f46c8ac8 100644 --- a/docs/manual/working/scalaGuide/main/json/ScalaJsonCombinators.md +++ b/docs/manual/working/scalaGuide/main/json/ScalaJsonCombinators.md @@ -136,6 +136,29 @@ One special case that our example model doesn't demonstrate is how to handle `Re @[reads-writes-recursive](code/ScalaJsonCombinatorsSpec.scala) +When defining a recursive codec with a Scala 3 `given`, use the Scala 3-only +`recursive` method instead. It supplies the codec being constructed as a +deferred contextual value, so codecs for recursive fields can be derived +without referring directly to the `given` during its initialization: + +```scala +final case class User(name: String, friends: List[User]) + +given OFormat[User] = OFormat.recursive { + ( + (__ \ "name").format[String] and + (__ \ "friends").format[List[User]] + )(User.apply, user => (user.name, user.friends)) +} +``` + +Equivalent methods are available on the `Reads`, `Writes`, `OWrites`, and +`Format` companion objects. The deferred codec can also be accessed within +the block using `summon[OFormat[User]]`. These methods defer codec +initialization; they do not make reading or writing deeply recursive values +stack-safe. Scala 2 code should continue to use the `lazyRead`, `lazyWrite`, +and `lazyFormat` methods. + ## Format [`Format[T]`](api/scala/play/api/libs/json/Format.html) is just a mix of the `Reads` and `Writes` traits and can be used for implicit conversion in place of its components. diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveFormat.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveFormat.scala index 155d28b53..52493cf9e 100644 --- a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveFormat.scala +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveFormat.scala @@ -7,6 +7,19 @@ package play.api.libs.json import scala.annotation.tailrec trait RecursiveFormat { self: Format.type => + + /** + * Constructs a `Format` for a recursive type. + * + * While `f` is evaluated, a deferred `Format[A]` is available as a + * contextual value. This lets formats derived inside `f` refer to the + * resulting format without forcing it during initialization. + * + * This method is available only in Scala 3. + * + * @tparam A the type read from and written as JSON + * @param f function that constructs the recursive format + */ final def recursive[A](f: Format[A] ?=> Format[A]): Format[A] = { lazy val res: Format[A] = f(using RecursiveFormat.DeferredFormat(() => res)) res diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOFormat.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOFormat.scala index 08c792e58..8e079adb0 100644 --- a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOFormat.scala +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOFormat.scala @@ -7,6 +7,19 @@ package play.api.libs.json import scala.annotation.tailrec trait RecursiveOFormat { self: OFormat.type => + + /** + * Constructs an `OFormat` for a recursive type. + * + * While `f` is evaluated, a deferred `OFormat[A]` is available as a + * contextual value. This lets object formats derived inside `f` refer to + * the resulting format without forcing it during initialization. + * + * This method is available only in Scala 3. + * + * @tparam A the type read from and written as a JSON object + * @param f function that constructs the recursive object format + */ final def recursive[A](f: OFormat[A] ?=> OFormat[A]): OFormat[A] = { lazy val res: OFormat[A] = f(using RecursiveOFormat.DeferredOFormat(() => res)) res diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOWrites.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOWrites.scala index b096295c6..9af2c2b52 100644 --- a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOWrites.scala +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveOWrites.scala @@ -7,6 +7,19 @@ package play.api.libs.json import scala.annotation.tailrec trait RecursiveOWrites { self: OWrites.type => + + /** + * Constructs an `OWrites` for a recursive type. + * + * While `f` is evaluated, a deferred `OWrites[A]` is available as a + * contextual value. This lets object writers derived inside `f` refer to + * the resulting writer without forcing it during initialization. + * + * This method is available only in Scala 3. + * + * @tparam A the type written as a JSON object + * @param f function that constructs the recursive object writer + */ final def recursive[A](f: OWrites[A] ?=> OWrites[A]): OWrites[A] = { lazy val res: OWrites[A] = f(using RecursiveOWrites.DeferredOWrites(() => res)) res diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveReads.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveReads.scala index 7f07dbece..a4b4bb5de 100644 --- a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveReads.scala +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveReads.scala @@ -7,6 +7,19 @@ package play.api.libs.json import scala.annotation.tailrec trait RecursiveReads { self: Reads.type => + + /** + * Constructs a `Reads` for a recursive type. + * + * While `f` is evaluated, a deferred `Reads[A]` is available as a + * contextual value. This lets readers derived inside `f` refer to the + * resulting reader without forcing it during initialization. + * + * This method is available only in Scala 3. + * + * @tparam A the type read from JSON + * @param f function that constructs the recursive reader + */ final def recursive[A](f: Reads[A] ?=> Reads[A]): Reads[A] = { lazy val res: Reads[A] = f(using RecursiveReads.DeferredReads(() => res)) res diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveWrites.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveWrites.scala index 745fd05dd..7ff2b0391 100644 --- a/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveWrites.scala +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/RecursiveWrites.scala @@ -7,6 +7,19 @@ package play.api.libs.json import scala.annotation.tailrec trait RecursiveWrites { self: Writes.type => + + /** + * Constructs a `Writes` for a recursive type. + * + * While `f` is evaluated, a deferred `Writes[A]` is available as a + * contextual value. This lets writers derived inside `f` refer to the + * resulting writer without forcing it during initialization. + * + * This method is available only in Scala 3. + * + * @tparam A the type written as JSON + * @param f function that constructs the recursive writer + */ final def recursive[A](f: Writes[A] ?=> Writes[A]): Writes[A] = { lazy val res: Writes[A] = f(using RecursiveWrites.DeferredWrites(() => res)) res