diff --git a/docs/manual/working/scalaGuide/main/json/ScalaJson.md b/docs/manual/working/scalaGuide/main/json/ScalaJson.md index 49ad7838b..e96694801 100644 --- a/docs/manual/working/scalaGuide/main/json/ScalaJson.md +++ b/docs/manual/working/scalaGuide/main/json/ScalaJson.md @@ -163,6 +163,18 @@ Readable: } ``` +### Using OutputStream utilities + +As with the String utilities, but writing to an `OutputStream` to avoid building the full string in memory. These examples use a `ByteArrayOutputStream` for illustration, though other stream types such as `FileOutputStream` will be more common. + +Minified: + +@[convert-to-stream](code/ScalaJsonSpec.scala) + +Readable: + +@[convert-to-stream-pretty](code/ScalaJsonSpec.scala) + ### Using JsValue.as/asOpt The simplest way to convert a `JsValue` to another type is using `JsValue.as[T](implicit fjs: Reads[T]): T`. This requires an implicit converter of type [`Reads[T]`](api/scala/play/api/libs/json/Reads.html) to convert a `JsValue` to `T` (the inverse of `Writes[T]`). As with `Writes`, the JSON API provides `Reads` for basic types. diff --git a/docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala b/docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala index 5861e03c9..9248ae4c5 100644 --- a/docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala +++ b/docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala @@ -6,6 +6,8 @@ package scalaguide.json import org.specs2.mutable.Specification +import java.io.ByteArrayOutputStream + class ScalaJsonSpec extends Specification { val sampleJson = { //#convert-from-string @@ -339,6 +341,26 @@ class ScalaJsonSpec extends Specification { readableString.must(contain("Bigwig")) } + "allow writing JsValue to OutputStream" in { + import play.api.libs.json._ + val json = sampleJson + + //#convert-to-stream + val minifiedStream = new ByteArrayOutputStream() + Json.writeToStream(json, minifiedStream) + //#convert-to-stream + + //#convert-to-stream-pretty + val readableStream = new ByteArrayOutputStream() + Json.prettyPrintToStream(json, readableStream) + //#convert-to-stream-pretty + + val minifiedString: String = minifiedStream.toString("UTF-8") + minifiedString.must(contain("Fiver")) + val readableString: String = readableStream.toString("UTF-8") + readableString.must(contain("Bigwig")) + } + "allow converting JsValue using as" in { val json = sampleJson diff --git a/play-json/js-native/src/main/scala/StaticBindingNonJvm.scala b/play-json/js-native/src/main/scala/StaticBindingNonJvm.scala index 78580500a..66312af88 100644 --- a/play-json/js-native/src/main/scala/StaticBindingNonJvm.scala +++ b/play-json/js-native/src/main/scala/StaticBindingNonJvm.scala @@ -54,9 +54,17 @@ private[json] object StaticBindingNonJvm { arraySep = ("[ ", ", ", " ]") ) + // TODO: Write to the stream when traversing JsValue without buffering the whole string. + def prettyPrintToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit = + stream.write(prettyPrint(jsValue).getBytes("UTF-8")) + def toBytes(jsValue: JsValue): Array[Byte] = generateFromJsValue(jsValue, false).getBytes("UTF-8") + // TODO: Write to the stream when traversing JsValue without buffering the whole string. + def writeToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit = + stream.write(toBytes(jsValue)) + def fromJs( jsValue: JsValue, escapeNonASCII: Boolean, diff --git a/play-json/js/src/main/scala/StaticBinding.scala b/play-json/js/src/main/scala/StaticBinding.scala index c24d8862e..676c095ad 100644 --- a/play-json/js/src/main/scala/StaticBinding.scala +++ b/play-json/js/src/main/scala/StaticBinding.scala @@ -26,8 +26,14 @@ object StaticBinding { def prettyPrint(jsValue: JsValue): String = StaticBindingNonJvm.prettyPrint(jsValue) + def prettyPrintToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit = + StaticBindingNonJvm.prettyPrintToStream(jsValue, stream) + def toBytes(jsValue: JsValue): Array[Byte] = StaticBindingNonJvm.toBytes(jsValue) + def writeToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit = + StaticBindingNonJvm.writeToStream(jsValue, stream) + @inline private[json] def fromString(s: String, escapeNonASCII: Boolean): String = if (!escapeNonASCII) JSON.stringify(s, null) else StaticBindingNonJvm.escapeStr(JSON.stringify(s, null)) diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala b/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala index a11763502..8b5be8db0 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala @@ -6,6 +6,8 @@ package play.api.libs.json import play.api.libs.json.jackson.JacksonJson +import java.io.OutputStream + object StaticBinding { /** Parses a [[JsValue]] from raw data. */ @@ -25,6 +27,12 @@ object StaticBinding { def prettyPrint(jsValue: JsValue): String = JacksonJson.get.prettyPrint(jsValue) + def prettyPrintToStream(jsValue: JsValue, stream: OutputStream): Unit = + JacksonJson.get.prettyPrintToStream(jsValue, stream) + def toBytes(jsValue: JsValue): Array[Byte] = JacksonJson.get.jsValueToBytes(jsValue) + + def writeToStream(jsValue: JsValue, stream: OutputStream): Unit = + JacksonJson.get.writeJsValueToStream(jsValue, stream) } diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala b/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala index 63a1e7f63..d0e2c0479 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala @@ -5,6 +5,7 @@ package play.api.libs.json.jackson import java.io.InputStream +import java.io.OutputStream import java.io.StringWriter import scala.annotation.switch @@ -313,7 +314,10 @@ private[play] case class JacksonJson(defaultMapperJsonConfig: JsonConfig) { this.currentMapper = mapper } - private def stringJsonGenerator(out: java.io.StringWriter) = + private def stringJsonGenerator(out: StringWriter) = + mapper().getFactory.createGenerator(out) + + private def stringJsonGenerator(out: OutputStream) = mapper().getFactory.createGenerator(out) def parseJsValue(data: Array[Byte]): JsValue = @@ -365,9 +369,21 @@ private[play] case class JacksonJson(defaultMapperJsonConfig: JsonConfig) { sw.getBuffer.toString } + def prettyPrintToStream(jsValue: JsValue, stream: OutputStream): Unit = { + val gen = stringJsonGenerator(stream).setPrettyPrinter( + new DefaultPrettyPrinter() + ) + val writer: ObjectWriter = mapper().writerWithDefaultPrettyPrinter() + + writer.writeValue(gen, jsValue) + } + def jsValueToBytes(jsValue: JsValue): Array[Byte] = mapper().writeValueAsBytes(jsValue) + def writeJsValueToStream(jsValue: JsValue, stream: OutputStream): Unit = + mapper().writeValue(stream, jsValue) + def jsValueToJsonNode(jsValue: JsValue): JsonNode = mapper().valueToTree(jsValue) diff --git a/play-json/native/src/main/scala/StaticBinding.scala b/play-json/native/src/main/scala/StaticBinding.scala index e62a5cbc9..21d92770f 100644 --- a/play-json/native/src/main/scala/StaticBinding.scala +++ b/play-json/native/src/main/scala/StaticBinding.scala @@ -38,8 +38,14 @@ object StaticBinding { def prettyPrint(jsValue: JsValue): String = StaticBindingNonJvm.prettyPrint(jsValue) + def prettyPrintToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit = + StaticBindingNonJvm.prettyPrintToStream(jsValue, stream) + def toBytes(jsValue: JsValue): Array[Byte] = StaticBindingNonJvm.toBytes(jsValue) + def writeToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit = + StaticBindingNonJvm.writeToStream(jsValue, stream) + @inline private[json] def fromString(s: String, escapeNonASCII: Boolean): String = { def escaped(c: Char) = c match { case '\b' => "\\b" 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 96c9d78bd..9157fc8cd 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,7 +4,7 @@ package play.api.libs.json -import java.io.InputStream +import java.io.{ InputStream, OutputStream } import scala.collection.mutable.{ Builder => MBuilder } @@ -87,6 +87,14 @@ sealed trait JsonFacade { */ def toBytes(json: JsValue): Array[Byte] + /** + * Writes a [[JsValue]] to an output stream. + * + * $jsonParam + * @param stream the stream to write to. + */ + def writeToStream(json: JsValue, stream: OutputStream): Unit + /** * Converts a [[JsValue]] to its string representation, * escaping all non-ascii characters using `\u005CuXXXX` syntax. @@ -140,6 +148,16 @@ sealed trait JsonFacade { */ def prettyPrint(json: JsValue): String + /** + * Converts a [[JsValue]] to its pretty string representation using default + * pretty printer (line feeds after each fields and 2-spaces indentation) and + * writes the result to an output stream. + * + * $jsonParam + * @param stream the stream to write to. + */ + def prettyPrintToStream(json: JsValue, stream: OutputStream): Unit + /** * Converts any writeable value to a [[JsValue]]. * @@ -186,7 +204,7 @@ sealed trait JsonFacade { * Helper functions to handle JsValues. * * @define macroOptions @tparam Opts the compile-time options - * @define macroTypeParam @tparam A the type for which the handler must be materialized + * @define macroTypeParam @tparam The type for which the handler must be materialized * @define macroWarning If any missing implicit is discovered, compiler will break with corresponding error. */ object Json extends JsonFacade with JsMacros with JsValueMacros { @@ -207,6 +225,8 @@ object Json extends JsonFacade with JsMacros with JsValueMacros { def toBytes(json: JsValue): Array[Byte] = StaticBinding.toBytes(json) + def writeToStream(json: JsValue, stream: OutputStream): Unit = StaticBinding.writeToStream(json, stream) + // We use unicode \u005C for a backlash in comments, because Scala will replace unicode escapes during lexing // anywhere in the program. def asciiStringify(json: JsValue): String = @@ -214,6 +234,8 @@ object Json extends JsonFacade with JsMacros with JsValueMacros { def prettyPrint(json: JsValue): String = StaticBinding.prettyPrint(json) + def prettyPrintToStream(json: JsValue, stream: OutputStream): Unit = StaticBinding.prettyPrintToStream(json, stream) + def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue = tjs.writes(o) def toJsObject[T](o: T)(implicit @@ -361,11 +383,15 @@ object Json extends JsonFacade with JsMacros with JsValueMacros { @inline def toBytes(json: JsValue): Array[Byte] = Json.toBytes(json) + @inline def writeToStream(json: JsValue, stream: OutputStream): Unit = Json.writeToStream(json, stream) + @inline def asciiStringify(json: JsValue): String = Json.asciiStringify(json) @inline def prettyPrint(json: JsValue): String = Json.prettyPrint(json) + @inline def prettyPrintToStream(json: JsValue, stream: OutputStream): Unit = Json.prettyPrintToStream(json, stream) + @inline def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue = Json.toJson[T](o) 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 04611cd9a..a479045d8 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 @@ -13,6 +13,8 @@ import org.scalacheck.Gen import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec +import java.io.ByteArrayOutputStream + class JsonSharedSpec extends AnyWordSpec with Matchers @@ -182,6 +184,29 @@ class JsonSharedSpec (success \ "price").mustEqual(JsDefined(JsString("2.5 €"))) } + + "write to output stream the UTF-8 representation" in json { js => + val json = js.parse(""" + |{ + | "name": "coffee", + | "symbol": "☕", + | "price": "2.5 €" + |} + """.stripMargin) + + val stream = new ByteArrayOutputStream() + js.writeToStream(json, stream) + val string = stream.toString("UTF-8") + val parsedJson = js.tryParse(string) + + parsedJson.isSuccess.mustEqual(true) + + val success = parsedJson.success.value + + (success \ "symbol").mustEqual(JsDefined(JsString("☕"))) + + (success \ "price").mustEqual(JsDefined(JsString("2.5 €"))) + } } "Complete JSON should create full object" when { @@ -309,7 +334,27 @@ class JsonSharedSpec ) js.prettyPrint(jo) - .replace("\r\n", "\n") + .mustEqual("""{ + "key1" : "toto", + "key2" : { + "key21" : "tata", + "key22" : 123 + }, + "key3" : [ 1, "tutu" ] +}""") + } + + "JSON pretty print to stream" in json { js => + def jo = js.obj( + "key1" -> "toto", + "key2" -> js.obj("key21" -> "tata", "key22" -> 123), + "key3" -> js.arr(1, "tutu") + ) + + val stream = new ByteArrayOutputStream() + js.prettyPrintToStream(jo, stream) + stream + .toString("UTF-8") .mustEqual("""{ "key1" : "toto", "key2" : {