Streaming APIs: JsonReader and JsonWriter

While Moshi's data binding via JsonAdapter is powerful and convenient for most use cases, sometimes you need lower-level access to the JSON stream. This can be for performance reasons, handling massive JSON documents, or implementing custom logic that doesn't map cleanly to objects.

Moshi provides two core classes for this: JsonReader for parsing and JsonWriter for generating JSON.

JsonReader

JsonReader allows you to read a JSON-encoded value as a stream of tokens. The stream includes literal values (strings, numbers, booleans, nulls) and the begin/end delimiters of objects and arrays.

You can process a JSON document token by token:

val json = """{
  "id": 912345678901,
  "text": "How do I read a JSON stream?",
  "user": {
    "name": "json_newb",
    "followers_count": 41
  }
}"""

val reader = JsonReader.of(Buffer().writeUtf8(json))

reader.beginObject()
while (reader.hasNext()) {
    when (reader.nextName()) {
        "id" -> println(reader.nextLong())
        "text" -> println(reader.nextString())
        "user" -> {
            reader.beginObject()
            while(reader.hasNext()) {
                when (reader.nextName()) {
                    "name" -> println(reader.nextString())
                    "followers_count" -> println(reader.nextInt())
                    else -> reader.skipValue()
                }
            }
            reader.endObject()
        }
        else -> reader.skipValue() // Important to skip unknown names
    }
}
reader.endObject()

JsonReader is a powerful tool for manual parsing and is the foundation upon which JsonAdapters are built.

Reading Raw JSON with nextSource()

A particularly useful method on JsonReader is nextSource(). It returns an Okio BufferedSource that streams the raw UTF-8 bytes of the next JSON value. This is extremely efficient for:

  • Extracting an embedded JSON document without parsing it.
  • Passing a JSON value to another library or system.
  • Processing a very large value (like a base64-encoded file) as a stream.

See the Raw JSON Values example for a practical use case.

JsonWriter

JsonWriter allows you to write a JSON-encoded value token by token.

val buffer = Buffer()
val writer = JsonWriter.of(buffer)

writer.beginObject()
writer.name("id").value(912345678901)
writer.name("text").value("How do I write a JSON stream?")
writer.name("user")
writer.beginObject()
writer.name("name").value("json_pro")
writer.name("followers_count").value(99)
writer.endObject()
writer.endObject()

println(buffer.readUtf8())
// Output: {"id":912345678901,"text":"How do I write a JSON stream?","user":{"name":"json_pro","followers_count":99}}

Writing Raw JSON with value(BufferedSource)

Symmetrical to JsonReader.nextSource(), JsonWriter.value(BufferedSource) allows you to write raw, pre-encoded JSON bytes directly into the stream. This is useful for embedding a JSON document that you already have as a string or byte stream without parsing and re-serializing it.

val rawJson = "{\"nested\":true}"

writer.beginObject()
writer.name("data")
writer.value(Buffer().writeUtf8(rawJson))
writer.endObject()

// Output: {"data":{"nested":true}}