Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom field names for tuples Reads/Writes #896

Merged
merged 1 commit into from
Jul 16, 2023
Merged
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 docs/manual/working/scalaGuide/main/json/ScalaJson.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,9 @@ To convert from JsValue to a model, you must define implicit `Reads[T]` where `T
@[sample-model](code/ScalaJsonSpec.scala)

@[convert-to-model](code/ScalaJsonSpec.scala)

### Using simple tuples

Simple JSON object can be reads as and writes from simple tuples.

@[handle-simple-tuples](code/ScalaJsonSpec.scala)
21 changes: 21 additions & 0 deletions docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -452,5 +452,26 @@ class ScalaJsonSpec extends Specification {
placeResult.must(beLike { case JsSuccess(Place(name, _, _), _) => name === "Watership Down" })
residentResult.must(beLike { case JsSuccess(Resident(name, _, _), _) => name === "Bigwig" })
}

"handle simple tuples" in {
//#handle-simple-tuples
import play.api.libs.json._

val tuple3Reads: Reads[(String, Int, Boolean)] =
Reads.tuple3[String, Int, Boolean]("name", "age", "isStudent")

val tuple3Writes: OWrites[(String, Int, Boolean)] =
OWrites.tuple3[String, Int, Boolean]("name", "age", "isStudent")

val tuple3ExampleJson: JsObject =
Json.obj("name" -> "Bob", "age" -> 30, "isStudent" -> false)

val tuple3Example = Tuple3("Bob", 30, false)

tuple3Writes.writes(tuple3Example) mustEqual tuple3ExampleJson

tuple3Reads.reads(tuple3ExampleJson) mustEqual JsSuccess(tuple3Example)
//#handle-simple-tuples
}
}
}
98 changes: 98 additions & 0 deletions play-json/shared/src/main/scala/play/api/libs/json/Reads.scala
Original file line number Diff line number Diff line change
Expand Up @@ -623,4 +623,102 @@ trait DefaultReads extends LowPriorityDefaultReads {
}

implicit val uuidReads: Reads[java.util.UUID] = new UUIDReader(false)

/**
* Reads a JSON object and constructs a tuple of two values,
* with custom names for the element fields.
*
* @param name1 the name of the first element `_1`
* @param name2 the name of the second element `_2`
* @tparam A the type for the first element
* @tparam B the type for the second element
*
* {{{
* val tuple2Reads: Reads[(String, Int)] = Reads.tuple2[String, Int]("name", "age")
*
* val tuple2ExampleJson = Json.obj("name" -> "Alice", "age" -> 25)
* val tuple2Result: JsResult[(String, Int)] = tuple2Reads.reads(tuple2ExampleJson)
* // JsSuccess(("Alice", 25))
* }}}
*/
def tuple2[A: Reads, B: Reads](name1: String, name2: String): Reads[(A, B)] =
Reads[(A, B)] { js =>
for {
_1 <- (js \ name1).validate[A]
_2 <- (js \ name2).validate[B]
} yield _1 -> _2
}

/**
* Reads a JSON object and constructs a tuple of three values,
* with custom names for the element fields.
*
* @param name1 the name of the first element `_1`
* @param name2 the name of the second element `_2`
* @param name3 the name of the third element `_3`
* @tparam A the type for the first element
* @tparam B the type for the second element
* @tparam C the type for the third element
*
* {{{
* val tuple3Reads: Reads[(String, Int, Boolean)] =
* Reads.tuple3[String, Int, Boolean]("name", "age", "isStudent")
*
* val tuple3ExampleJson: JsValue =
* Json.obj("name" -> "Alice", "age" -> 25, "isStudent" -> true)
*
* val tuple3Result: JsResult[(String, Int, Boolean)] =
* tuple3Reads.reads(tuple3ExampleJson)
* // JsSuccess(("Alice", 25, true))
* }}}
*/
def tuple3[A: Reads, B: Reads, C: Reads](name1: String, name2: String, name3: String): Reads[(A, B, C)] =
Reads[(A, B, C)] { js =>
for {
_1 <- (js \ name1).validate[A]
_2 <- (js \ name2).validate[B]
_3 <- (js \ name3).validate[C]
} yield Tuple3(_1, _2, _3)
}

/**
* Reads a JSON object and constructs a tuple of four values,
* with custom names for the element fields.
*
* @param name1 the name of the first element `_1`
* @param name2 the name of the second element `_2`
* @param name3 the name of the third element `_3`
* @param name4 the name of the fourth element `_4`
* @tparam A the type for the first element
* @tparam B the type for the second element
* @tparam C the type for the third element
* @tparam D the type for the fourth element
*
* {{{
* val tuple4Reads: Reads[(String, Int, Boolean, Double)] =
* Reads.tuple4[String, Int, Boolean, Double](
* "name", "age", "isStudent", "score")
*
* val tuple4ExampleJson: JsValue = Json.obj(
* "name" -> "Alice", "age" -> 25, "isStudent" -> true, "score" -> 78.9)
*
* val tuple4Result: JsResult[(String, Int, Boolean, Double)] =
* tuple4Reads.reads(tuple4ExampleJson)
* // JsSuccess(("Alice", 25, true, 78.9))
* }}}
*/
def tuple4[A: Reads, B: Reads, C: Reads, D: Reads](
name1: String,
name2: String,
name3: String,
name4: String
): Reads[(A, B, C, D)] =
Reads[(A, B, C, D)] { js =>
for {
_1 <- (js \ name1).validate[A]
_2 <- (js \ name2).validate[B]
_3 <- (js \ name3).validate[C]
_4 <- (js \ name4).validate[D]
} yield Tuple4(_1, _2, _3, _4)
}
}
71 changes: 71 additions & 0 deletions play-json/shared/src/main/scala/play/api/libs/json/Writes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,77 @@ object OWrites extends PathWrites with ConstraintWrites {
OWrites[A] { a =>
f(a, w.writes(a))
}

/**
* Writes a tuple of two values to a JSON object, with custom field names.
*
* @param name1 the name of the first field
* @param name2 the name of the second field
* @tparam A the type of the first value
* @tparam B the type of the second value
*
* {{{
* val tuple2Writes: OWrites[(String, Int)] =
* OWrites.tuple2[String, Int]("name", "age")
*
* tuple2Writes.writes("Bob" -> 30) // {"name":"Bob","age":30}
* }}}
*/
def tuple2[A: Writes, B: Writes](name1: String, name2: String): OWrites[(A, B)] = OWrites[(A, B)] { case (a, b) =>
Json.obj(name1 -> a, name2 -> b)
}

/**
* Writes a tuple of three values to a JSON object, with custom field names.
*
* @param name1 the name of the first field
* @param name2 the name of the second field
* @param name3 the name of the third field
* @tparam A the type of the first value
* @tparam B the type of the second value
* @tparam C the type of the third value
*
* {{{
* val tuple3Writes: OWrites[(String, Int, Boolean)] =
* OWrites.tuple3[String, Int, Boolean]("name", "age", "isStudent")
*
* tuple3Writes.writes(("Bob", 30, false))
* // {"name":"Bob","age":30,"isStudent":false}
* }}}
*/
def tuple3[A: Writes, B: Writes, C: Writes](name1: String, name2: String, name3: String): OWrites[(A, B, C)] =
OWrites[(A, B, C)] { case (a, b, c) =>
Json.obj(name1 -> a, name2 -> b, name3 -> c)
}

/**
* Writes a tuple of four values to a JSON object, with custom field names.
*
* @param name1 the name of the first field
* @param name2 the name of the second field
* @param name3 the name of the third field
* @param name4 the name of the fourth field
* @tparam A the type of the first value
* @tparam B the type of the second value
* @tparam C the type of the third value
* @tparam D the type of the fourth value
*
* {{{
* val tuple4Writes: OWrites[(String, Int, Boolean, Double)] =
* OWrites.tuple4[String, Int, Boolean, Double]("name", "age", "isStudent", "score")
*
* tuple4Writes.writes(("Bob", 30, false, 91.2))
* // {"name":"Bob","age":30,"isStudent":false,"score":91.2}
* }}}
*/
def tuple4[A: Writes, B: Writes, C: Writes, D: Writes](
name1: String,
name2: String,
name3: String,
name4: String
): OWrites[(A, B, C, D)] = OWrites[(A, B, C, D)] { case (a, b, c, d) =>
Json.obj(name1 -> a, name2 -> b, name3 -> c, name4 -> d)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,34 @@ final class ReadsSharedSpec extends AnyWordSpec with Matchers with Inside {
}
}

"Tuple" should {
"be read with custom element names" when {
"tuple2" in {
val reads = Reads.tuple2[String, Float]("name", "score")

Json.obj("name" -> "Foo", "score" -> 1.23F).validate(reads).mustEqual(JsSuccess("Foo" -> 1.23F))
}

"tuple3" in {
val reads = Reads.tuple3[String, Float, Int]("name", "score", "age")

Json
.obj("name" -> "Foo", "age" -> 10, "score" -> 1.23F)
.validate(reads)
.mustEqual(JsSuccess(Tuple3("Foo", 1.23F, 10)))
}

"tuple4" in {
val reads = Reads.tuple4[String, Float, Int, Seq[String]]("name", "score", "age", "aliases")

Json
.obj("name" -> "Foo", "aliases" -> Seq("Bar"), "age" -> 10, "score" -> 1.23F)
.validate(reads)
.mustEqual(JsSuccess(Tuple4("Foo", 1.23F, 10, Seq("Bar"))))
}
}
}

"Identity reads" should {
def success[T <: JsValue](fixture: T)(implicit r: Reads[T], ct: scala.reflect.ClassTag[T]) =
s"be resolved for $fixture as ${ct.runtimeClass.getSimpleName}" in {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,29 @@ final class WritesSharedSpec extends AnyWordSpec with Matchers {
}
}

"Tuples" should {
"be written with custom field names" when {
"tuple2" in {
val w = OWrites.tuple2[String, Double]("name", "score")

w.writes("Foo" -> 23.4D).mustEqual(Json.obj("name" -> "Foo", "score" -> 23.4D))
}

"tuple3" in {
val w = OWrites.tuple3[String, Int, Boolean]("name", "age", "isStudent")

w.writes(("Alice", 25, true)).mustEqual(Json.obj("name" -> "Alice", "age" -> 25, "isStudent" -> true))
}

"tuple4" in {
val w = OWrites.tuple4[String, Int, Boolean, Double]("name", "age", "isStudent", "score")

w.writes(("Bob", 30, false, 78.9D))
.mustEqual(Json.obj("name" -> "Bob", "age" -> 30, "isStudent" -> false, "score" -> 78.9D))
}
}
}

"Identity writes" should {
import scala.reflect.ClassTag
import scala.language.higherKinds
Expand Down