Skip to content

Commit

Permalink
Merge pull request #876 from cchantep/feature/jsobj_builder
Browse files Browse the repository at this point in the history
Add Json.newBuilder utility
  • Loading branch information
mkurz authored May 30, 2023
2 parents 8de0b07 + 0e2bc2b commit 6a44c27
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 22 deletions.
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

0 comments on commit 6a44c27

Please sign in to comment.