Reading/transforming/writing JSON data

In this cookbook we will demonstrate an example of how the JSON tools provided by fs2-data can be used to build a mini jq-like CLI tool.

High-level overview

The general approach to reading/parsing/transforming/generating data with fs2-data can be summarized as follows:

graph LR
Reading(Reading) --> Parsing --> Transforming --> Printing --> Writing(Writing)

The Reading and Writing steps are not specific to fs2-data but rely on pure fs2 operators or other compatible libraries. The Parsing, Transforming, and Printing phases will use the tools provided by fs2-data-json and more specifically:

In general the Transforming step can use whatever operator fits your purpose, from fs2 or any other fs2-based library. But in our case the only transformation will be performed by the query.

Basic implementation

Reading and writing

In this example, we will read the content from a sample JSON file and write the result to stdout. To this end, we will use the operators and pipes provided by fs2-io.

import cats.effect.IO
import cats.effect.unsafe.implicits.global

import fs2.io.file.{Files, Path}
import fs2.io.stdout
import fs2.text.utf8

Files[IO]
  .readUtf8(Path("site/cookbooks/data/json/sample.json"))
  .through(utf8.encode[IO])
  .through(stdout)
  .compile
  .drain
  .unsafeRunSync()

This snippet is pure fs2 and does not involve fs2-data at any point.

Parsing and printing

The next step would be to parse and render the JSON data, using the appropriate fs2-data pipes. This can be achieved this way:

import fs2.data.json

Files[IO]
  .readUtf8(Path("site/cookbooks/data/json/sample.json"))
  .through(json.tokens) // parsing JSON input
  .through(json.render.prettyPrint()) // pretty printing JSON stream
  .through(utf8.encode[IO])
  .through(stdout)
  .compile
  .drain
  .unsafeRunSync()

Transforming

So far the only thing that the code does is to format the input into the output. Looking at the input, we see that it consists in an array of objects containing several fields. Let's say we are interested in the name and language fields. For each element in the array we would like to emit an object with both fields, but the name oned should be renamed full_name.

To this end we can write the following query using the jq interpolator:

import fs2.data.json.jq.literals._

val query = jq""".[] | { "full_name": .name, "language": .language }"""
// query: json.jq.Jq = Iterator(
//   filter = Identity,
//   inner = Obj(
//     prefix = Identity,
//     fields = List(
//       ("full_name", Field(name = "name")),
//       ("language", Field(name = "language"))
//     )
//   )
// )

The query can now be compiled into a Pipe:

import fs2.data.json.jq.Compiler

val queryCompiler = Compiler[IO]
// queryCompiler: Compiler[IO] = fs2.data.json.jq.internal.ESPJqCompiler@d920a7c

val queryPipe = queryCompiler.compile(query).unsafeRunSync()
// queryPipe: fs2.package.Pipe[IO, json.Token, json.Token] = <function1>

Now this pipe can be used to transform the data within the previous pipeline

Files[IO]
  .readUtf8(Path("site/cookbooks/data/json/sample.json"))
  .through(json.tokens)
  .through(queryPipe) // the transformation using the query pipe
  .through(json.render.prettyPrint())
  .through(utf8.encode[IO])
  .through(stdout)
  .compile
  .drain
  .unsafeRunSync()

And you get the result of the query execution printed to stdout.

Running the full example

The full code can be found in the repository in the JqLike object. This example uses decline to parse the CLI options.

It compiles for all three supported platforms:

$ sbt exampleJqJVM/assembly
$ java -jar examples/jqlike/.jvm/target/scala-2.13/jq-like.jar -q '.[] | { "full_name": .name, "language": .language }' -f site/cookbooks/data/json/sample.json
$ sbt exampleJqNative/nativeLink
$ examples/jqlike/.native/target/scala-2.13/jq-like-out -q '.[] | { "full_name": .name, "language": .language }' -f site/cookbooks/data/json/sample.json
$ sbt exampleJqJS/fastLinkJS
$ node examples/jqlike/.js/target/scala-2.13/jq-like-fastopt/main.js -q '.[] | { "full_name": .name, "language": .language }' -f site/cookbooks/data/json/sample.json