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

Add Json.newBuilder utility #876

Merged
merged 2 commits into from
May 30, 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
4 changes: 4 additions & 0 deletions docs/manual/working/scalaGuide/main/json/ScalaJson.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ Represents a path into a `JsValue` structure, analogous to XPath for XML. This i

@[convert-from-factory](code/ScalaJsonSpec.scala)

You also can use `Json.newBuilder` to create `JsObject`:

@[object-builder](code/ScalaJsonSpec.scala)

### Using Writes converters

Scala to `JsValue` conversion is performed by the utility method `Json.toJson[T](T)(implicit writes: Writes[T])`. This functionality depends on a converter of type [`Writes[T]`](api/scala/play/api/libs/json/Writes.html) which can convert a `T` to a `JsValue`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

package scalaguide.json

import play.api.libs.json.Json
import org.specs2.mutable.Specification

//#valueClass
Expand Down
74 changes: 56 additions & 18 deletions docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ class ScalaJsonSpec extends Specification {
"parse json" in {
import play.api.libs.json._
val json = sampleJson
(json \ "name").get.must_==(JsString("Watership Down"))
(json \ "location" \ "lat").get.must_==(JsNumber(51.235685))

(json \ "name").toOption mustEqual Some(JsString("Watership Down"))
(json \ "location" \ "lat").toOption mustEqual Some(JsNumber(51.235685))
}

"allow constructing json using case classes" in {
Expand Down Expand Up @@ -78,17 +79,15 @@ class ScalaJsonSpec extends Specification {
)
)
//#convert-from-classes
(json \ "name").get.must_==(JsString("Watership Down"))

(json \ "name").toOption mustEqual Some(JsString("Watership Down"))
}

"allow constructing json using factory methods" in {
//#convert-from-factory
import play.api.libs.json.JsNull
import play.api.libs.json.Json
import play.api.libs.json.JsString
import play.api.libs.json.JsValue
import play.api.libs.json.{JsNull,Json,JsString,JsObject}

val json: JsValue = Json.obj(
val json: JsObject = Json.obj(
"name" -> "Watership Down",
"location" -> Json.obj("lat" -> 51.235685, "long" -> -1.309197),
"residents" -> Json.arr(
Expand All @@ -105,7 +104,44 @@ class ScalaJsonSpec extends Specification {
)
)
//#convert-from-factory
(json \ "name").get.must_==(JsString("Watership Down"))

(json \ "name").toOption mustEqual Some(JsString("Watership Down"))
}

"allow constructing json using builder" in {
//#object-builder
import play.api.libs.json.{ JsNull, Json, JsString, JsObject }

def asJson(active: Boolean): JsObject = {
val builder = Json.newBuilder

builder ++= Seq(
"name" -> "Watership Down",
"location" -> Json.obj(
"lat" -> 51.235685D, "long" -> -1.309197D))

if (active) {
builder += "active" -> true
}

builder += "residents" -> Seq(
Json.obj(
"name" -> "Fiver",
"age" -> 4,
"role" -> JsNull
),
Json.obj(
"name" -> "Bigwig",
"age" -> 6,
"role" -> "Owsla"
))

builder.result()
}
//#object-builder

(asJson(true) \ "name").
toOption mustEqual Some(JsString("Watership Down"))
}

"allow converting simple types" in {
Expand Down Expand Up @@ -171,7 +207,7 @@ class ScalaJsonSpec extends Specification {
val json = Json.toJson(place)
//#convert-from-model

(json \ "name").get === JsString("Watership Down")
(json \ "name").toOption === Some(JsString("Watership Down"))
}

"allow converting models preferred" in {
Expand Down Expand Up @@ -211,27 +247,28 @@ class ScalaJsonSpec extends Specification {
val json = Json.toJson(place)
//#convert-from-model

(json \ "name").get === JsString("Watership Down")
(json \ "name").toOption === Some(JsString("Watership Down"))
}

"allow traversing JsValue tree" in {
import play.api.libs.json._
val json = sampleJson

//#traverse-simple-path
val lat = (json \ "location" \ "lat").get
// returns JsNumber(51.235685)
val bigwig = (json \ "residents" \ 1).get
// returns {"name":"Bigwig","age":6,"role":"Owsla"}
val lat = (json \ "location" \ "lat").toOption
// returns some JsNumber(51.235685)
val bigwig = (json \ "residents" \ 1).toOption
// returns some {"name":"Bigwig","age":6,"role":"Owsla"}

//#traverse-simple-path

val expected = Json.parse(
"""{"name":"Bigwig","age":6,"role":"Owsla"}"""
)
bigwig.mustEqual(expected)

lat === JsNumber(51.235685)
bigwig mustEqual Some(expected)

lat === Some(JsNumber(51.235685))

//#traverse-recursive-path
val names = json \\ "name"
Expand Down Expand Up @@ -282,7 +319,8 @@ class ScalaJsonSpec extends Specification {
case _: NoSuchElementException =>
}

(bigwig \ "name").get === JsString("Bigwig")
bigwig.flatMap(
obj => (obj \ "name").toOption) === Some(JsString("Bigwig"))
}

"allow converting JsValue to String" in {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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 scala.collection.mutable.{ Builder => MBuilder }

private[json] final class JsObjectBuilder extends MBuilder[(String, Json.JsValueWrapper), JsObject] {

private val fs = Map.newBuilder[String, JsValue]

def addOne(elem: (String, Json.JsValueWrapper)): this.type = {
val (name, wrapped) = elem

fs += (name -> Json.unwrap(wrapped))

this
}

override def addAll(xs: IterableOnce[(String, Json.JsValueWrapper)]): this.type = {
xs.iterator.foreach(addOne)

this
}

override def knownSize: Int = fs.knownSize

override def sizeHint(size: Int): Unit = {
fs.sizeHint(size)
}

def clear(): Unit = {
fs.clear()
}

def result(): JsObject = JsObject(fs.result())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 scala.collection.mutable.{ Builder => MBuilder }

private[json] final class JsObjectBuilder extends MBuilder[(String, Json.JsValueWrapper), JsObject] {

private val fs = Map.newBuilder[String, JsValue]

def +=(elem: (String, Json.JsValueWrapper)): this.type = {
val (name, wrapped) = elem

fs += (name -> Json.unwrap(wrapped))

this
}

override def ++=(xs: TraversableOnce[(String, Json.JsValueWrapper)]): this.type = {
xs.foreach(`+=`)

this
}

def knownSize: Int = fs.result().size

def clear(): Unit = {
fs.clear()
}

def result(): JsObject = JsObject(fs.result())
}
48 changes: 45 additions & 3 deletions play-json/shared/src/main/scala/play/api/libs/json/Json.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package play.api.libs.json

import scala.collection.mutable.{ Builder => MBuilder }

import java.io.InputStream

/**
Expand Down Expand Up @@ -226,12 +228,52 @@ object Json extends JsonFacade with JsMacros with JsValueMacros {
implicit def toJsFieldJsValueWrapper[T](field: T)(implicit w: Writes[T]): JsValueWrapper =
JsValueWrapperImpl(w.writes(field))

def obj(fields: (String, JsValueWrapper)*): JsObject = JsObject(fields.map(f => (f._1, unwrap(f._2))))
def obj(fields: (String, JsValueWrapper)*): JsObject = JsObject(fields.map { case (name, wrapped) =>
name -> unwrap(wrapped)
})

/**
* Returns a JSON object builder.
*
* {{{
* import play.api.libs.json.{ Json, JsObject }
*
* // Create a new builder
* val builder: JsObjectBuilder = JsObject.newBuilder
*
* // Add key-value pairs to the builder
* builder += ("name" -> "John Doe")
* builder += ("age" -> 25)
*
* // Clear the builder
* builder.clear()
*
* // Add more key-value pairs
* builder += ("email" -> "john.doe@example.com")
* builder += ("address" -> "123 Street")
*
* // Build the final JsObject
* val result: JsObject = builder.result()
*
* // Print the resulting JsObject
* println(result)
* }}}
*
* This will output:
* {{{
* {"email":"john.doe@example.com","address":"123 Street"}
* }}}
*/
def newBuilder: MBuilder[(String, Json.JsValueWrapper), JsObject] =
new JsObjectBuilder()

def arr(items: JsValueWrapper*): JsArray = JsArray(items.iterator.map(unwrap).toArray[JsValue])

// Passed nulls will typecheck without needing the implicit conversion, so they need to checked at runtime
private def unwrap(wrapper: JsValueWrapper) = wrapper match {
/*
* Passed nulls will typecheck without needing the implicit conversion,
* so they need to checked at runtime.
*/
private[json] def unwrap(wrapper: JsValueWrapper) = wrapper match {
case null => JsNull
case JsValueWrapperImpl(value) => value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,22 @@ class JsonSharedSpec extends AnyWordSpec with Matchers with org.scalatestplus.sc
"js.toJsObject(peach)(writes)".mustNot(typeCheck)
}

"create object using builder" in {
val builder = Json.newBuilder

builder += ("name" -> "John Doe")
builder += ("age" -> 25)

val result: JsObject = builder.result()

result.mustEqual(
Json.obj(
"name" -> "John Doe",
"age" -> 25
)
)
}

"convert to a byte array containing the UTF-8 representation" in json { js =>
val json = js.parse("""
|{
Expand Down