diff --git a/docs/manual/working/scalaGuide/main/json/ScalaJson.md b/docs/manual/working/scalaGuide/main/json/ScalaJson.md index c5df420be..a4283bcfe 100644 --- a/docs/manual/working/scalaGuide/main/json/ScalaJson.md +++ b/docs/manual/working/scalaGuide/main/json/ScalaJson.md @@ -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`. diff --git a/docs/manual/working/scalaGuide/main/json/code-2/Scala2JsonAutomatedSpec.scala b/docs/manual/working/scalaGuide/main/json/code-2/Scala2JsonAutomatedSpec.scala index 329389122..822863a44 100644 --- a/docs/manual/working/scalaGuide/main/json/code-2/Scala2JsonAutomatedSpec.scala +++ b/docs/manual/working/scalaGuide/main/json/code-2/Scala2JsonAutomatedSpec.scala @@ -4,7 +4,6 @@ package scalaguide.json -import play.api.libs.json.Json import org.specs2.mutable.Specification //#valueClass diff --git a/docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala b/docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala index b341833cc..e0b62e163 100644 --- a/docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala +++ b/docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala @@ -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 { @@ -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( @@ -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 { @@ -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 { @@ -211,7 +247,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 traversing JsValue tree" in { @@ -219,19 +255,20 @@ class ScalaJsonSpec extends Specification { 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" @@ -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 { diff --git a/play-json/shared/src/main/scala-2.13+/play/api/libs/json/JsObjectBuilder.scala b/play-json/shared/src/main/scala-2.13+/play/api/libs/json/JsObjectBuilder.scala new file mode 100644 index 000000000..a6e984372 --- /dev/null +++ b/play-json/shared/src/main/scala-2.13+/play/api/libs/json/JsObjectBuilder.scala @@ -0,0 +1,38 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +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()) +} diff --git a/play-json/shared/src/main/scala-2.13-/play/api/libs/json/JsObjectBuilder.scala b/play-json/shared/src/main/scala-2.13-/play/api/libs/json/JsObjectBuilder.scala new file mode 100644 index 000000000..70c41d616 --- /dev/null +++ b/play-json/shared/src/main/scala-2.13-/play/api/libs/json/JsObjectBuilder.scala @@ -0,0 +1,34 @@ +/* + * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. + */ + +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()) +} diff --git a/play-json/shared/src/main/scala/play/api/libs/json/Json.scala b/play-json/shared/src/main/scala/play/api/libs/json/Json.scala index 59dac64a9..a6425ee85 100644 --- a/play-json/shared/src/main/scala/play/api/libs/json/Json.scala +++ b/play-json/shared/src/main/scala/play/api/libs/json/Json.scala @@ -4,6 +4,8 @@ package play.api.libs.json +import scala.collection.mutable.{ Builder => MBuilder } + import java.io.InputStream /** @@ -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 } diff --git a/play-json/shared/src/test/scala/play/api/libs/json/JsonSharedSpec.scala b/play-json/shared/src/test/scala/play/api/libs/json/JsonSharedSpec.scala index 355f697a8..bc6e080a6 100644 --- a/play-json/shared/src/test/scala/play/api/libs/json/JsonSharedSpec.scala +++ b/play-json/shared/src/test/scala/play/api/libs/json/JsonSharedSpec.scala @@ -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(""" |{