Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
16 changes: 16 additions & 0 deletions docs/manual/working/scalaGuide/main/json/ScalaJsonAutomated.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

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)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json

private[json] trait DerivedFormat {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json

private[json] trait DerivedReads {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json

private[json] trait DerivedWrites {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json

private[json] trait DerivedFormat {
inline def derived[T]: OFormat[T] = Json.format[T]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json

private[json] trait DerivedReads {
inline def derived[T]: Reads[T] = Json.reads[T]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json

private[json] trait DerivedWrites {
inline def derived[T]: OWrites[T] = Json.writes[T]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
*/

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
}
Loading