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 support for writing to an output stream #1127

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
12 changes: 12 additions & 0 deletions docs/manual/working/scalaGuide/main/json/ScalaJson.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions docs/manual/working/scalaGuide/main/json/code/ScalaJsonSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions play-json/js-native/src/main/scala/StaticBindingNonJvm.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions play-json/js/src/main/scala/StaticBinding.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -286,7 +287,10 @@ private[json] case class JacksonJson(jsonConfig: JsonConfig) {

private val jsonFactory = new JsonFactory(mapper)

private def stringJsonGenerator(out: java.io.StringWriter) =
private def stringJsonGenerator(out: StringWriter) =
jsonFactory.createGenerator(out)

private def stringJsonGenerator(out: OutputStream) =
jsonFactory.createGenerator(out)

def parseJsValue(data: Array[Byte]): JsValue =
Expand Down Expand Up @@ -338,9 +342,21 @@ private[json] case class JacksonJson(jsonConfig: 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)

Expand Down
6 changes: 6 additions & 0 deletions play-json/native/src/main/scala/StaticBinding.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 28 additions & 2 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,7 +4,7 @@

package play.api.libs.json

import java.io.InputStream
import java.io.{ InputStream, OutputStream }

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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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]].
*
Expand Down Expand Up @@ -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 {
Expand All @@ -207,13 +225,17 @@ 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 =
StaticBinding.generateFromJsValue(json, true)

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
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -178,6 +180,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 {
Expand Down Expand Up @@ -313,7 +338,29 @@ class JsonSharedSpec
"key22" : 123
},
"key3" : [ 1, "tutu" ]
}""")
}""".replace("\r\n", "\n"))
}

"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")
.replace("\r\n", "\n")
.mustEqual("""{
"key1" : "toto",
"key2" : {
"key21" : "tata",
"key22" : 123
},
"key3" : [ 1, "tutu" ]
}""".replace("\r\n", "\n"))
}

"asciiStringify should escape non-ascii characters" in json { js =>
Expand Down
Loading