Skip to content

Commit 0b73c45

Browse files
mkurzhere-abarany
andauthored
Add support for writing to an output stream (#1257)
Added JsonFacade functions writeToStream and prettyPrintToStream to support writing to an OutputStream without first writing the full string to memory. The JVM implementation forwards these calls to the appropriate functions in Jackson. For now, the non-JVM implementations will build up the full string in memory. Ideally the strings should be written out as they are built up, but this would require a refactoring of the fromJs() function to support an interface to feed each string value and minimize the number of conversions to UTF-8 (for toBytes and writing to stream) or avoid conversions (for String). For now, this fulfills the interface guarantee while providing the same level of functionality as before. Fixes #1126 Co-authored-by: Aaron Barany <[email protected]>
1 parent 3147d5f commit 0b73c45

File tree

9 files changed

+153
-4
lines changed

9 files changed

+153
-4
lines changed

docs/manual/working/scalaGuide/main/json/ScalaJson.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,18 @@ Readable:
163163
}
164164
```
165165

166+
### Using OutputStream utilities
167+
168+
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.
169+
170+
Minified:
171+
172+
@[convert-to-stream](code/ScalaJsonSpec.scala)
173+
174+
Readable:
175+
176+
@[convert-to-stream-pretty](code/ScalaJsonSpec.scala)
177+
166178
### Using JsValue.as/asOpt
167179

168180
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.

docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package scalaguide.json
66

77
import org.specs2.mutable.Specification
88

9+
import java.io.ByteArrayOutputStream
10+
911
class ScalaJsonSpec extends Specification {
1012
val sampleJson = {
1113
//#convert-from-string
@@ -339,6 +341,26 @@ class ScalaJsonSpec extends Specification {
339341
readableString.must(contain("Bigwig"))
340342
}
341343

344+
"allow writing JsValue to OutputStream" in {
345+
import play.api.libs.json._
346+
val json = sampleJson
347+
348+
//#convert-to-stream
349+
val minifiedStream = new ByteArrayOutputStream()
350+
Json.writeToStream(json, minifiedStream)
351+
//#convert-to-stream
352+
353+
//#convert-to-stream-pretty
354+
val readableStream = new ByteArrayOutputStream()
355+
Json.prettyPrintToStream(json, readableStream)
356+
//#convert-to-stream-pretty
357+
358+
val minifiedString: String = minifiedStream.toString("UTF-8")
359+
minifiedString.must(contain("Fiver"))
360+
val readableString: String = readableStream.toString("UTF-8")
361+
readableString.must(contain("Bigwig"))
362+
}
363+
342364
"allow converting JsValue using as" in {
343365
val json = sampleJson
344366

play-json/js-native/src/main/scala/StaticBindingNonJvm.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,17 @@ private[json] object StaticBindingNonJvm {
5454
arraySep = ("[ ", ", ", " ]")
5555
)
5656

57+
// TODO: Write to the stream when traversing JsValue without buffering the whole string.
58+
def prettyPrintToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
59+
stream.write(prettyPrint(jsValue).getBytes("UTF-8"))
60+
5761
def toBytes(jsValue: JsValue): Array[Byte] =
5862
generateFromJsValue(jsValue, false).getBytes("UTF-8")
5963

64+
// TODO: Write to the stream when traversing JsValue without buffering the whole string.
65+
def writeToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
66+
stream.write(toBytes(jsValue))
67+
6068
def fromJs(
6169
jsValue: JsValue,
6270
escapeNonASCII: Boolean,

play-json/js/src/main/scala/StaticBinding.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,14 @@ object StaticBinding {
2626

2727
def prettyPrint(jsValue: JsValue): String = StaticBindingNonJvm.prettyPrint(jsValue)
2828

29+
def prettyPrintToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
30+
StaticBindingNonJvm.prettyPrintToStream(jsValue, stream)
31+
2932
def toBytes(jsValue: JsValue): Array[Byte] = StaticBindingNonJvm.toBytes(jsValue)
3033

34+
def writeToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
35+
StaticBindingNonJvm.writeToStream(jsValue, stream)
36+
3137
@inline private[json] def fromString(s: String, escapeNonASCII: Boolean): String =
3238
if (!escapeNonASCII) JSON.stringify(s, null) else StaticBindingNonJvm.escapeStr(JSON.stringify(s, null))
3339

play-json/jvm/src/main/scala/play/api/libs/json/StaticBinding.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package play.api.libs.json
66

77
import play.api.libs.json.jackson.JacksonJson
88

9+
import java.io.OutputStream
10+
911
object StaticBinding {
1012

1113
/** Parses a [[JsValue]] from raw data. */
@@ -25,6 +27,12 @@ object StaticBinding {
2527

2628
def prettyPrint(jsValue: JsValue): String = JacksonJson.get.prettyPrint(jsValue)
2729

30+
def prettyPrintToStream(jsValue: JsValue, stream: OutputStream): Unit =
31+
JacksonJson.get.prettyPrintToStream(jsValue, stream)
32+
2833
def toBytes(jsValue: JsValue): Array[Byte] =
2934
JacksonJson.get.jsValueToBytes(jsValue)
35+
36+
def writeToStream(jsValue: JsValue, stream: OutputStream): Unit =
37+
JacksonJson.get.writeJsValueToStream(jsValue, stream)
3038
}

play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package play.api.libs.json.jackson
66

77
import java.io.InputStream
8+
import java.io.OutputStream
89
import java.io.StringWriter
910

1011
import scala.annotation.switch
@@ -313,7 +314,10 @@ private[play] case class JacksonJson(defaultMapperJsonConfig: JsonConfig) {
313314
this.currentMapper = mapper
314315
}
315316

316-
private def stringJsonGenerator(out: java.io.StringWriter) =
317+
private def stringJsonGenerator(out: StringWriter) =
318+
mapper().getFactory.createGenerator(out)
319+
320+
private def stringJsonGenerator(out: OutputStream) =
317321
mapper().getFactory.createGenerator(out)
318322

319323
def parseJsValue(data: Array[Byte]): JsValue =
@@ -365,9 +369,21 @@ private[play] case class JacksonJson(defaultMapperJsonConfig: JsonConfig) {
365369
sw.getBuffer.toString
366370
}
367371

372+
def prettyPrintToStream(jsValue: JsValue, stream: OutputStream): Unit = {
373+
val gen = stringJsonGenerator(stream).setPrettyPrinter(
374+
new DefaultPrettyPrinter()
375+
)
376+
val writer: ObjectWriter = mapper().writerWithDefaultPrettyPrinter()
377+
378+
writer.writeValue(gen, jsValue)
379+
}
380+
368381
def jsValueToBytes(jsValue: JsValue): Array[Byte] =
369382
mapper().writeValueAsBytes(jsValue)
370383

384+
def writeJsValueToStream(jsValue: JsValue, stream: OutputStream): Unit =
385+
mapper().writeValue(stream, jsValue)
386+
371387
def jsValueToJsonNode(jsValue: JsValue): JsonNode =
372388
mapper().valueToTree(jsValue)
373389

play-json/native/src/main/scala/StaticBinding.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,14 @@ object StaticBinding {
3838

3939
def prettyPrint(jsValue: JsValue): String = StaticBindingNonJvm.prettyPrint(jsValue)
4040

41+
def prettyPrintToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
42+
StaticBindingNonJvm.prettyPrintToStream(jsValue, stream)
43+
4144
def toBytes(jsValue: JsValue): Array[Byte] = StaticBindingNonJvm.toBytes(jsValue)
4245

46+
def writeToStream(jsValue: JsValue, stream: java.io.OutputStream): Unit =
47+
StaticBindingNonJvm.writeToStream(jsValue, stream)
48+
4349
@inline private[json] def fromString(s: String, escapeNonASCII: Boolean): String = {
4450
def escaped(c: Char) = c match {
4551
case '\b' => "\\b"

play-json/shared/src/main/scala/play/api/libs/json/Json.scala

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
package play.api.libs.json
66

7-
import java.io.InputStream
7+
import java.io.{ InputStream, OutputStream }
88

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

@@ -87,6 +87,14 @@ sealed trait JsonFacade {
8787
*/
8888
def toBytes(json: JsValue): Array[Byte]
8989

90+
/**
91+
* Writes a [[JsValue]] to an output stream.
92+
*
93+
* $jsonParam
94+
* @param stream the stream to write to.
95+
*/
96+
def writeToStream(json: JsValue, stream: OutputStream): Unit
97+
9098
/**
9199
* Converts a [[JsValue]] to its string representation,
92100
* escaping all non-ascii characters using `\u005CuXXXX` syntax.
@@ -140,6 +148,16 @@ sealed trait JsonFacade {
140148
*/
141149
def prettyPrint(json: JsValue): String
142150

151+
/**
152+
* Converts a [[JsValue]] to its pretty string representation using default
153+
* pretty printer (line feeds after each fields and 2-spaces indentation) and
154+
* writes the result to an output stream.
155+
*
156+
* $jsonParam
157+
* @param stream the stream to write to.
158+
*/
159+
def prettyPrintToStream(json: JsValue, stream: OutputStream): Unit
160+
143161
/**
144162
* Converts any writeable value to a [[JsValue]].
145163
*
@@ -186,7 +204,7 @@ sealed trait JsonFacade {
186204
* Helper functions to handle JsValues.
187205
*
188206
* @define macroOptions @tparam Opts the compile-time options
189-
* @define macroTypeParam @tparam A the type for which the handler must be materialized
207+
* @define macroTypeParam @tparam The type for which the handler must be materialized
190208
* @define macroWarning If any missing implicit is discovered, compiler will break with corresponding error.
191209
*/
192210
object Json extends JsonFacade with JsMacros with JsValueMacros {
@@ -207,13 +225,17 @@ object Json extends JsonFacade with JsMacros with JsValueMacros {
207225

208226
def toBytes(json: JsValue): Array[Byte] = StaticBinding.toBytes(json)
209227

228+
def writeToStream(json: JsValue, stream: OutputStream): Unit = StaticBinding.writeToStream(json, stream)
229+
210230
// We use unicode \u005C for a backlash in comments, because Scala will replace unicode escapes during lexing
211231
// anywhere in the program.
212232
def asciiStringify(json: JsValue): String =
213233
StaticBinding.generateFromJsValue(json, true)
214234

215235
def prettyPrint(json: JsValue): String = StaticBinding.prettyPrint(json)
216236

237+
def prettyPrintToStream(json: JsValue, stream: OutputStream): Unit = StaticBinding.prettyPrintToStream(json, stream)
238+
217239
def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue = tjs.writes(o)
218240

219241
def toJsObject[T](o: T)(implicit
@@ -361,11 +383,15 @@ object Json extends JsonFacade with JsMacros with JsValueMacros {
361383

362384
@inline def toBytes(json: JsValue): Array[Byte] = Json.toBytes(json)
363385

386+
@inline def writeToStream(json: JsValue, stream: OutputStream): Unit = Json.writeToStream(json, stream)
387+
364388
@inline def asciiStringify(json: JsValue): String =
365389
Json.asciiStringify(json)
366390

367391
@inline def prettyPrint(json: JsValue): String = Json.prettyPrint(json)
368392

393+
@inline def prettyPrintToStream(json: JsValue, stream: OutputStream): Unit = Json.prettyPrintToStream(json, stream)
394+
369395
@inline def toJson[T](o: T)(implicit tjs: Writes[T]): JsValue =
370396
Json.toJson[T](o)
371397

play-json/shared/src/test/scala/play/api/libs/json/JsonSharedSpec.scala

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import org.scalacheck.Gen
1313
import org.scalatest.matchers.must.Matchers
1414
import org.scalatest.wordspec.AnyWordSpec
1515

16+
import java.io.ByteArrayOutputStream
17+
1618
class JsonSharedSpec
1719
extends AnyWordSpec
1820
with Matchers
@@ -182,6 +184,29 @@ class JsonSharedSpec
182184

183185
(success \ "price").mustEqual(JsDefined(JsString("2.5 €")))
184186
}
187+
188+
"write to output stream the UTF-8 representation" in json { js =>
189+
val json = js.parse("""
190+
|{
191+
| "name": "coffee",
192+
| "symbol": "☕",
193+
| "price": "2.5 €"
194+
|}
195+
""".stripMargin)
196+
197+
val stream = new ByteArrayOutputStream()
198+
js.writeToStream(json, stream)
199+
val string = stream.toString("UTF-8")
200+
val parsedJson = js.tryParse(string)
201+
202+
parsedJson.isSuccess.mustEqual(true)
203+
204+
val success = parsedJson.success.value
205+
206+
(success \ "symbol").mustEqual(JsDefined(JsString("")))
207+
208+
(success \ "price").mustEqual(JsDefined(JsString("2.5 €")))
209+
}
185210
}
186211

187212
"Complete JSON should create full object" when {
@@ -309,7 +334,27 @@ class JsonSharedSpec
309334
)
310335

311336
js.prettyPrint(jo)
312-
.replace("\r\n", "\n")
337+
.mustEqual("""{
338+
"key1" : "toto",
339+
"key2" : {
340+
"key21" : "tata",
341+
"key22" : 123
342+
},
343+
"key3" : [ 1, "tutu" ]
344+
}""")
345+
}
346+
347+
"JSON pretty print to stream" in json { js =>
348+
def jo = js.obj(
349+
"key1" -> "toto",
350+
"key2" -> js.obj("key21" -> "tata", "key22" -> 123),
351+
"key3" -> js.arr(1, "tutu")
352+
)
353+
354+
val stream = new ByteArrayOutputStream()
355+
js.prettyPrintToStream(jo, stream)
356+
stream
357+
.toString("UTF-8")
313358
.mustEqual("""{
314359
"key1" : "toto",
315360
"key2" : {

0 commit comments

Comments
 (0)