diff --git a/README.md b/README.md index 914fd2b48..e7cbc963a 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,12 @@ I can also define a `Format` that does both: implicit val residentFormat = Json.format[Resident] ``` +Alternatively, on Scala 3.x, I can declare that my class `derives Reads`, `derives Writes` or `derives Format`: + +```scala +case class Resident(name: String, age: Int, role: Option[String]) derives Format +``` + With the `Reads` and/or `Writes` in scope, I can then easily convert my class using `toJson` and `fromJson` ### Constructing `Reads` and `Writes` diff --git a/docs/manual/working/scalaGuide/main/json/ScalaJsonAutomated.md b/docs/manual/working/scalaGuide/main/json/ScalaJsonAutomated.md index e6341a57b..f85691dbd 100644 --- a/docs/manual/working/scalaGuide/main/json/ScalaJsonAutomated.md +++ b/docs/manual/working/scalaGuide/main/json/ScalaJsonAutomated.md @@ -74,6 +74,22 @@ Then the macros are able generate `Reads[T]`, `OWrites[T]` or `OFormat[T]`. @[auto-JSON-sealed-trait](code/ScalaJsonAutomatedSpec.scala) +## Type Class Derivation + +When using Scala 3, you can use [Type Class Derivation](https://docs.scala-lang.org/scala3/reference/contextual/derivation.html) to derive `Reads[T]`, `Writes[T]` and `Format[T]`. + +This support has exactly the same requirements and limitations as the macros described above. + +For example, the following case class automatically derives `Reads[Resident]`: + +@[reads-model](code-3/Scala3JsonAutomatedSpec.scala) + +You can also use `derives Writes` or `derives Format`. + +When deriving `Reads[T]`, `Writes[T]` and `Format[T]` for traits, you must declare that each subclass `derives` the appropriate trait: + +@[reads-trait](code-3/Scala3JsonAutomatedSpec.scala) + ## Custom Naming Strategies To use a custom Naming Strategy you need to define a implicit `JsonConfiguration` object and a `JsonNaming`. diff --git a/docs/manual/working/scalaGuide/main/json/code-3/Scala3JsonAutomatedSpec.scala b/docs/manual/working/scalaGuide/main/json/code-3/Scala3JsonAutomatedSpec.scala new file mode 100644 index 000000000..b3110bcfa --- /dev/null +++ b/docs/manual/working/scalaGuide/main/json/code-3/Scala3JsonAutomatedSpec.scala @@ -0,0 +1,53 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package scalaguide.json + +import org.specs2.mutable.Specification +import play.api.libs.json._ + +object Scala3JsonAutomatedSpec { + //#reads-model + case class Resident( + name: String, + age: Int, + specialism: Option[String] + ) derives Reads + //#reads-model + + val residentJson = Json.parse( + """{ + "name" : "Fiver", + "age" : 4 + }""" + ) + + val sampleResident = Resident("Fiver", 4, None) + + //#reads-trait + sealed trait Role derives Reads + case object Admin extends Role derives Reads + case class Contributor(organization: String) extends Role derives Reads + //#reads-trait + + val contributorJson = Json.obj("_type" -> "scalaguide.json.Scala3JsonAutomatedSpec.Contributor", "organization" -> "Foo") + val sampleContributor = Contributor("Foo") +} + +class Scala3JsonAutomatedSpec extends Specification { + import Scala3JsonAutomatedSpec._ + + "Scala 3 JSON automated" should { + "for case class" >> { + "derive a Reads" in { + residentJson.as[Resident].must_===(sampleResident) + } + } + "for trait" >> { + "derive a Reads if every subclass derives Reads" in { + contributorJson.as[Role].must_===(sampleContributor) + } + } + } +} diff --git a/play-json/shared/src/main/scala-2/play/api/libs/json/DerivedFormat.scala b/play-json/shared/src/main/scala-2/play/api/libs/json/DerivedFormat.scala new file mode 100644 index 000000000..f353e79a7 --- /dev/null +++ b/play-json/shared/src/main/scala-2/play/api/libs/json/DerivedFormat.scala @@ -0,0 +1,7 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +private[json] trait DerivedFormat {} diff --git a/play-json/shared/src/main/scala-2/play/api/libs/json/DerivedReads.scala b/play-json/shared/src/main/scala-2/play/api/libs/json/DerivedReads.scala new file mode 100644 index 000000000..04f425048 --- /dev/null +++ b/play-json/shared/src/main/scala-2/play/api/libs/json/DerivedReads.scala @@ -0,0 +1,7 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +private[json] trait DerivedReads {} diff --git a/play-json/shared/src/main/scala-2/play/api/libs/json/DerivedWrites.scala b/play-json/shared/src/main/scala-2/play/api/libs/json/DerivedWrites.scala new file mode 100644 index 000000000..ace178311 --- /dev/null +++ b/play-json/shared/src/main/scala-2/play/api/libs/json/DerivedWrites.scala @@ -0,0 +1,7 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +private[json] trait DerivedWrites {} diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/DerivedFormat.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/DerivedFormat.scala new file mode 100644 index 000000000..87ee4bf9f --- /dev/null +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/DerivedFormat.scala @@ -0,0 +1,9 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +private[json] trait DerivedFormat { + inline def derived[T]: OFormat[T] = Json.format[T] +} diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/DerivedReads.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/DerivedReads.scala new file mode 100644 index 000000000..d599281bf --- /dev/null +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/DerivedReads.scala @@ -0,0 +1,9 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +private[json] trait DerivedReads { + inline def derived[T]: Reads[T] = Json.reads[T] +} diff --git a/play-json/shared/src/main/scala-3/play/api/libs/json/DerivedWrites.scala b/play-json/shared/src/main/scala-3/play/api/libs/json/DerivedWrites.scala new file mode 100644 index 000000000..e1fbf5702 --- /dev/null +++ b/play-json/shared/src/main/scala-3/play/api/libs/json/DerivedWrites.scala @@ -0,0 +1,9 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +private[json] trait DerivedWrites { + inline def derived[T]: OWrites[T] = Json.writes[T] +} 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..0e8cc1279 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 @@ -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 DerivedFormat { 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 bf59bf822..7ac0616d3 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,7 @@ 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 DerivedReads { 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..c1eaa2eab 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 @@ -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 DerivedWrites { val constraints: ConstraintWrites = this val path: PathWrites = this diff --git a/play-json/shared/src/test/scala-3/play/api/libs/json/DerivesSyntaxSpec.scala b/play-json/shared/src/test/scala-3/play/api/libs/json/DerivesSyntaxSpec.scala new file mode 100644 index 000000000..e97bcc4ce --- /dev/null +++ b/play-json/shared/src/test/scala-3/play/api/libs/json/DerivesSyntaxSpec.scala @@ -0,0 +1,97 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +package play.api.libs.json + +import org.scalatest.EitherValues +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec + +final class DerivesSyntaxSpec extends AnyWordSpec with Matchers with EitherValues { + "Derives syntax" should { + "derive Format using macros" when { + "used with standalone case classes" in { + val input = DerivesFormat.StandaloneCaseClass(1) + val json = Json.toJson(input) + json mustBe Json.obj("intField" -> 1) + Json.fromJson[DerivesFormat.StandaloneCaseClass](json).asEither.value mustBe input + } + + "used with trait subtypes that are case objects" in { + val input = DerivesFormat.SomeCaseObject + val json = Json.toJson[DerivesFormat.SomeTrait](input) + json mustBe Json.obj("_type" -> "play.api.libs.json.DerivesFormat.SomeCaseObject") + Json.fromJson[DerivesFormat.SomeTrait](json).asEither.value mustBe input + } + + "used with trait subtypes that are case classes" in { + val input = DerivesFormat.SomeCaseClass("hello") + val json = Json.toJson[DerivesFormat.SomeTrait](input) + json mustBe Json.obj("_type" -> "play.api.libs.json.DerivesFormat.SomeCaseClass", "stringField" -> "hello") + Json.fromJson[DerivesFormat.SomeTrait](json).asEither.value mustBe input + } + } + + "derive Reads using macros" when { + "used with standalone case classes" in { + val input = Json.obj("intField" -> 16) + val expected = DerivesReads.StandaloneCaseClass(16) + Json.fromJson[DerivesReads.StandaloneCaseClass](input).asEither.value mustBe expected + } + + "used with trait subtypes that are case objects" in { + val input = Json.obj("_type" -> "play.api.libs.json.DerivesReads.SomeCaseObject") + val expected = DerivesReads.SomeCaseObject + Json.fromJson[DerivesReads.SomeTrait](input).asEither.value mustBe expected + } + + "used with trait subtypes that are case classes" in { + val input = Json.obj("_type" -> "play.api.libs.json.DerivesReads.SomeCaseClass", "stringField" -> "abc") + val expected = DerivesReads.SomeCaseClass("abc") + Json.fromJson[DerivesReads.SomeTrait](input).asEither.value mustBe expected + } + } + + "derive Writes using macros" when { + "used with standalone case classes" in { + val input = DerivesWrites.StandaloneCaseClass(42) + val expected = Json.obj("intField" -> 42) + Json.toJson(input) mustBe expected + } + + "used with trait subtypes that are case objects" in { + val input = DerivesWrites.SomeCaseObject + val expected = Json.obj("_type" -> "play.api.libs.json.DerivesWrites.SomeCaseObject") + Json.toJson[DerivesWrites.SomeTrait](input) mustBe expected + } + + "used with trait subtypes that are case classes" in { + val input = DerivesWrites.SomeCaseClass("def") + val expected = Json.obj("_type" -> "play.api.libs.json.DerivesWrites.SomeCaseClass", "stringField" -> "def") + Json.toJson[DerivesWrites.SomeTrait](input) mustBe expected + } + } + } +} + +object DerivesFormat { + case class StandaloneCaseClass(intField: Int) derives Format + sealed trait SomeTrait derives Format + case class SomeCaseClass(stringField: String) extends SomeTrait derives Format + case object SomeCaseObject extends SomeTrait derives Format +} + +object DerivesReads { + case class StandaloneCaseClass(intField: Int) derives Reads + sealed trait SomeTrait derives Reads + case class SomeCaseClass(stringField: String) extends SomeTrait derives Reads + case object SomeCaseObject extends SomeTrait derives Reads +} + +object DerivesWrites { + case class StandaloneCaseClass(intField: Int) derives Writes + sealed trait SomeTrait derives Writes + case class SomeCaseClass(stringField: String) extends SomeTrait derives Writes + case object SomeCaseObject extends SomeTrait derives Writes +}