Flows: JSON
Dependency:
"com.softwaremill.ox" %% "flow-json" % "1.0.9"
Ox can parse a Flow[Chunk[Byte]] into values, and render values back into byte chunks, either as newline-delimited JSON
(NDJSON) or as a single top-level JSON array. Values are converted using
jsoniter-scala, so each operation needs a JsonValueCodec[T] in scope.
Codecs are derived at compile time, which needs one more dependency:
"com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.41.2"
import com.github.plokhotnyuk.jsoniter_scala.macros.ConfiguredJsonValueCodec
case class Event(id: Long, message: String) derives ConfiguredJsonValueCodec
ConfiguredJsonValueCodec extends JsonValueCodec, so the derived instance is used by all operations below. derives
needs jsoniter-scala-macros at runtime; to keep it compile-only, scope it as compile-internal and write
given JsonValueCodec[Event] = JsonCodecMaker.make instead.
A derived codec rejects a JSON null. To read one, derive the codec for Option[T], which maps null to None.
NDJSON
flow.parseNdjson
Splits the input into LF-delimited records and parses each one. Lines holding only spaces, tabs and CRs are skipped, and CRLF endings, a final record without an LF and one leading byte-order mark are accepted. Every other line must hold exactly one JSON value; content after the value fails the flow.
A record is buffered until its LF, or until the end of the input for the final one. Records may be at most
maxRecordBytes long, 32 MiB by default; a longer one fails the flow.
A record which fits in a single incoming chunk is parsed straight from it, without buffering. To get this, set the
source’s chunk size above the typical record size, e.g. Flow.fromFile(path, chunkSize = 65536) (8 KB by default).
import ox.flow.Flow
import ox.flow.json.*
import java.nio.file.Paths
Flow
.fromFile(Paths.get("events.ndjson"))
.parseNdjson[Event]()
.filter(_.id > 1)
.runForeach(println)
flow.renderNdjson
Writes each value followed by an LF, including the last one. The WriterConfig must not indent, so that each value stays
on one line; line breaks inside strings are escaped anyway.
Each value becomes one chunk, and runToFile/runToOutputStream do one write per chunk. With many small records, write
to a BufferedOutputStream using runToOutputStream, instead of writing to a file directly.
import ox.flow.Flow
import ox.flow.json.*
import java.nio.file.Paths
Flow
.fromValues(Event(1, "created"), Event(2, "updated"))
.renderNdjson()
.runToFile(Paths.get("events.ndjson"))
JSON arrays
flow.parseJsonArray
Reads exactly one top-level array, emitting its elements as they are parsed. A byte-order mark is not stripped. An
incomplete array and a non-array top-level value fail the flow; a top-level null is read as an empty array. Content
after the array fails the flow as well, so the flow completes only once the source ends.
Reading the input creates an asynchronous boundary, so chunks are consumed ahead of downstream demand.
import ox.flow.Flow
import ox.flow.json.*
import java.nio.file.Paths
Flow
.fromFile(Paths.get("events.json"))
.parseJsonArray[Event]()
.runForeach(println)
flow.renderJsonArray
Writes [ and ] around the comma-separated values; an empty flow renders as []. If the flow fails after output has
started, the output is left as an incomplete array.
import ox.flow.Flow
import ox.flow.json.*
import java.nio.file.Paths
Flow
.fromValues(Event(1, "created"), Event(2, "updated"))
.renderJsonArray()
.runToFile(Paths.get("events.json"))