From 81977ba309e1842bdadd51ede683795030a1d9cc Mon Sep 17 00:00:00 2001 From: xuwei-k <6b656e6a69@gmail.com> Date: Fri, 24 Apr 2026 21:16:48 +0900 Subject: [PATCH] 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 00000000..80089c1a --- /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 00000000..68b41755 --- /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 00000000..dc359576 --- /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 00000000..77bbdde9 --- /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 00000000..6c7303ed --- /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 00000000..155d28b5 --- /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 00000000..08c792e5 --- /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 00000000..b096295c --- /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 00000000..7f07dbec --- /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 00000000..745fd05d --- /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 d077beb4..ee76f851 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 7a216a98..5d6313ae 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 7deadf4f..e33c8cef 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 00000000..2e9890d4 --- /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(), + ), + ) + ) + ) + ) + } +}