Interactive JSONiq tutorial

This tutorial introduces the JSONiq language, which declaratively manipulates JSON data. It is an interactive notebook that works with Rumble running as a server in the background.

Installation of Spark, Python and Jupyter

If you are running this notebook in Jupyter already, we assume you have already installed Python, Jupyter as well as probably Spark. If not, the easiest is to use a distribution such as Anaconda, which installs everything for you on any operating system.

Then, all you need to do is download this notebook locally, then run

jupyter notebook JSONiq-tutorial.ipynb

in the same directory to open it.

One more thing to know: Spark only works with Java 8. If you use Java 11 or 14, you will get an error message. It is possible to install several versions of Java on the same machine, and if such is the case, you simply need to switch your JAVA_HOME environment variable to the Java 8 home.

Download Rumble and run it as a server

For this notebook to work, we need two more very simple steps to prepare Rumble. You can execute them on your command line (note that you need to open a new command line window, so that jupyter can continue to run meanwhile).

Download rumble with:

wget https://github.com/RumbleDB/rumble/releases/download/v1.6.3/spark-rumble-1.6.3.jar

(you can also download it manually by using this link in your browser.

Then, you can start a rumble server by simply executing:

spark-submit spark-rumble-1.6.3.jar --server yes --port 9090

You should leave it running for all the duration of this tutorial. Then, you can interrupt it with Ctrl-C.

If the port is already taken, you can change 9090 to another number (e.g., 8001, 8081, 9091, etc). If you do so, make sure to make the same change in the cell assigning to the Python server variable further down.

The setup described above will run Spark and Rumble on your laptop. However, if you wish instead to play with a larger cluster (like Amazone EMR or Azure), it is straightforward! A ready-to-use Spark cluster takes just a few minutes to set up with a few easy clicks. When you have done so, just run the above two commands on the master of the cluster. When you connect (with SSH), you need to add -L 9090:localhost:9090 to the ssh command to establish an SSH tunnel right to your laptop. Then, you you can use this notebook in exactly the same way as if it were local.

In order for JSONiq to run successfully, you need to execute the following cell (without changing it):


In [46]:
import requests
import json
import time
from IPython.core.magic import register_line_cell_magic

@register_line_cell_magic
def rumble(line, cell=None):
    if cell is None:
        data = line
    else:
        data = cell

    start = time.time()                                                         
    response = json.loads(requests.post(server, data=data).text)                   
    end = time.time()                                                              
    print("Took: %s ms" % (end - start))

    if 'warning' in response:
        print(json.dumps(response['warning']))
    if 'values' in response:
        for e in response['values']:
            print(json.dumps(e))
    elif 'error-message' in response:
        return response['error-message']
    else:
        return response

As well as this one (where you need to change the port from 9090 to another value if you used a different --port parameter):


In [47]:
server='http://localhost:9090/jsoniq'

Now we are all set!

JSON

As explained on the official JSON Web site, JSON is a lightweight data-interchange format designed for humans as well as for computers. It supports as values:

  • objects (string-to-value maps)
  • arrays (ordered sequences of values)
  • strings
  • numbers
  • booleans (true, false)
  • null

JSONiq provides declarative querying and updating capabilities on JSON data.

Elevator Pitch

JSONiq is based on XQuery, which is a W3C standard (like XML and HTML). XQuery is a very powerful declarative language that originally manipulates XML data, but it turns out that it is also a very good fit for manipulating JSON natively. JSONiq, since it extends XQuery, is a very powerful general-purpose declarative programming language. Our experience is that, for the same task, you will probably write about 80% less code compared to imperative languages like JavaScript, Python or Ruby. Additionally, you get the benefits of strong type checking without actually having to write type declarations. Here is an appetizer before we start the tutorial from scratch.


In [48]:
%%rumble

let $stores :=
[
  { "store number" : 1, "state" : "MA" },
  { "store number" : 2, "state" : "MA" },
  { "store number" : 3, "state" : "CA" },
  { "store number" : 4, "state" : "CA" }
]
let $sales := [
   { "product" : "broiler", "store number" : 1, "quantity" : 20  },
   { "product" : "toaster", "store number" : 2, "quantity" : 100 },
   { "product" : "toaster", "store number" : 2, "quantity" : 50 },
   { "product" : "toaster", "store number" : 3, "quantity" : 50 },
   { "product" : "blender", "store number" : 3, "quantity" : 100 },
   { "product" : "blender", "store number" : 3, "quantity" : 150 },
   { "product" : "socks", "store number" : 1, "quantity" : 500 },
   { "product" : "socks", "store number" : 2, "quantity" : 10 },
   { "product" : "shirt", "store number" : 3, "quantity" : 10 }
]
let $join :=
  for $store in $stores[], $sale in $sales[]
  where $store."store number" = $sale."store number"
  return {
    "nb" : $store."store number",
    "state" : $store.state,
    "sold" : $sale.product
  }
return [$join]


Took: 0.011076211929321289 ms
[{"nb": 1, "state": "MA", "sold": "broiler"}, {"nb": 1, "state": "MA", "sold": "socks"}, {"nb": 2, "state": "MA", "sold": "toaster"}, {"nb": 2, "state": "MA", "sold": "toaster"}, {"nb": 2, "state": "MA", "sold": "socks"}, {"nb": 3, "state": "CA", "sold": "toaster"}, {"nb": 3, "state": "CA", "sold": "blender"}, {"nb": 3, "state": "CA", "sold": "blender"}, {"nb": 3, "state": "CA", "sold": "shirt"}]

And here you go

Actually, you already knew some JSONiq

The first thing you need to know is that a well-formed JSON document is a JSONiq expression as well. This means that you can copy-and-paste any JSON document into a query. The following are JSONiq queries that are "idempotent" (they just output themselves):


In [49]:
%%rumble
{ "pi" : 3.14, "sq2" : 1.4 }


Took: 0.006221771240234375 ms
{"pi": 3.14, "sq2": 1.4}

In [50]:
%%rumble
[ 2, 3, 5, 7, 11, 13 ]


Took: 0.0064008235931396484 ms
[2, 3, 5, 7, 11, 13]

In [51]:
%%rumble
{
      "operations" : [
        { "binary" : [ "and", "or"] },
        { "unary" : ["not"] }
      ],
      "bits" : [
        0, 1
      ]
    }


Took: 0.008172035217285156 ms
{"operations": [{"binary": ["and", "or"]}, {"unary": ["not"]}], "bits": [0, 1]}

In [52]:
%%rumble
[ { "Question" : "Ultimate" }, ["Life", "the universe", "and everything"] ]


Took: 0.006453990936279297 ms
[{"Question": "Ultimate"}, ["Life", "the universe", "and everything"]]

This works with objects, arrays (even nested), strings, numbers, booleans, null.

It also works the other way round: if your query outputs an object or an array, you can use it as a JSON document. JSONiq is a declarative language. This means that you only need to say what you want - the compiler will take care of the how.

In the above queries, you are basically saying: I want to output this JSON content, and here it is.

JSONiq basics

The real JSONiq Hello, World!

Wondering what a hello world program looks like in JSONiq? Here it is:


In [53]:
%%rumble
"Hello, World!"


Took: 0.0066568851470947266 ms
"Hello, World!"

Not surprisingly, it outputs the string "Hello, World!".

Numbers and arithmetic operations

Okay, so, now, you might be thinking: "What is the use of this language if it just outputs what I put in?" Of course, JSONiq can more than that. And still in a declarative way. Here is how it works with numbers:


In [54]:
%%rumble
2 + 2


Took: 0.0072820186614990234 ms
4

In [55]:
%%rumble
 (38 + 2) div 2 + 11 * 2


Took: 0.0064237117767333984 ms
42

(mind the division operator which is the "div" keyword. The slash operator has different semantics).

Like JSON, JSONiq works with decimals and doubles:


In [56]:
%%rumble
 6.022e23 * 42


Took: 0.007363080978393555 ms
25292400000000000000000000

Logical operations

JSONiq supports boolean operations.


In [57]:
%%rumble
true and false


Took: 0.006527900695800781 ms
false

In [58]:
%%rumble
(true or false) and (false or true)


Took: 0.007046222686767578 ms
true

The unary not is also available:


In [59]:
%%rumble
not true


Took: 0.006941080093383789 ms
false

Strings

JSONiq is capable of manipulating strings as well, using functions:


In [60]:
%%rumble
concat("Hello ", "Captain ", "Kirk")


Took: 0.005676984786987305 ms
"Hello Captain Kirk"

In [61]:
%%rumble
substring("Mister Spock", 8, 5)


Took: 0.00574493408203125 ms
"Spock"

JSONiq comes up with a rich string function library out of the box, inherited from its base language. These functions are listed here (actually, you will find many more for numbers, dates, etc).

Sequences

Until now, we have only been working with single values (an object, an array, a number, a string, a boolean). JSONiq supports sequences of values. You can build a sequence using commas:


In [62]:
%%rumble
 (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)


Took: 0.0066449642181396484 ms
1
2
3
4
5
6
7
8
9
10

In [63]:
%%rumble
1, true, 4.2e1, "Life"


Took: 0.00654292106628418 ms
1
true
42
"Life"

The "to" operator is very convenient, too:


In [64]:
%%rumble
 (1 to 100)


Took: 0.006345033645629883 ms
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

Some functions even work on sequences:


In [65]:
%%rumble
sum(1 to 100)


Took: 0.005728006362915039 ms
5050

In [66]:
%%rumble
string-join(("These", "are", "some", "words"), "-")


Took: 0.0058438777923583984 ms
"These-are-some-words"

In [67]:
%%rumble
count(10 to 20)


Took: 0.0066111087799072266 ms
11

In [68]:
%%rumble
avg(1 to 100)


Took: 0.005938053131103516 ms
50.5

Unlike arrays, sequences are flat. The sequence (3) is identical to the integer 3, and (1, (2, 3)) is identical to (1, 2, 3).

A bit more in depth

Variables

You can bind a sequence of values to a (dollar-prefixed) variable, like so:


In [69]:
%%rumble
let $x := "Bearing 3 1 4 Mark 5. "
return concat($x, "Engage!")


Took: 0.007143735885620117 ms
"Bearing 3 1 4 Mark 5. Engage!"

In [70]:
%%rumble
let $x := ("Kirk", "Picard", "Sisko")
return string-join($x, " and ")


Took: 0.006165742874145508 ms
"Kirk and Picard and Sisko"

You can bind as many variables as you want:


In [71]:
%%rumble
let $x := 1
let $y := $x * 2
let $z := $y + $x
return ($x, $y, $z)


Took: 0.006880044937133789 ms
1
2
3

and even reuse the same name to hide formerly declared variables:


In [72]:
%%rumble
let $x := 1
let $x := $x + 2
let $x := $x + 3
return $x


Took: 0.006127119064331055 ms
6

Iteration

In a way very similar to let, you can iterate over a sequence of values with the "for" keyword. Instead of binding the entire sequence of the variable, it will bind each value of the sequence in turn to this variable.


In [73]:
%%rumble
for $i in 1 to 10
return $i * 2


Took: 0.006555080413818359 ms
2
4
6
8
10
12
14
16
18
20

More interestingly, you can combine fors and lets like so:


In [74]:
%%rumble
let $sequence := 1 to 10
for $value in $sequence
let $square := $value * 2
return $square


Took: 0.006516933441162109 ms
2
4
6
8
10
12
14
16
18
20

and even filter out some values:


In [75]:
%%rumble
let $sequence := 1 to 10
for $value in $sequence
let $square := $value * 2
where $square < 10
return $square


Took: 0.0077419281005859375 ms
2
4
6
8

Note that you can only iterate over sequences, not arrays. To iterate over an array, you can obtain the sequence of its values with the [] operator, like so:


In [76]:
%%rumble
[1, 2, 3][]


Took: 0.006000041961669922 ms
1
2
3

Conditions

You can make the output depend on a condition with an if-then-else construct:


In [77]:
%%rumble
for $x in 1 to 10
return if ($x < 5) then $x
                   else -$x


Took: 0.0064771175384521484 ms
1
2
3
4
-5
-6
-7
-8
-9
-10

Note that the else clause is required - however, it can be the empty sequence () which is often when you need if only the then clause is relevant to you.

Composability of Expressions

Now that you know of a couple of elementary JSONiq expressions, you can combine them in more elaborate expressions. For example, you can put any sequence of values in an array:


In [78]:
%%rumble
[ 1 to 10 ]


Took: 0.007096052169799805 ms
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Or you can dynamically compute the value of object pairs (or their key):


In [79]:
%%rumble
{
      "Greeting" : (let $d := "Mister Spock"
                    return concat("Hello, ", $d)),
      "Farewell" : string-join(("Live", "long", "and", "prosper"),
                               " ")
}


Took: 0.007810831069946289 ms
{"Greeting": "Hello, Mister Spock", "Farewell": "Live long and prosper"}

You can dynamically generate object singletons (with a single pair):


In [80]:
%%rumble
{ concat("Integer ", 2) : 2 * 2 }


Took: 0.006745100021362305 ms
{"Integer 2": 4}

and then merge lots of them into a new object with the {| |} notation:


In [81]:
%%rumble
{|
    for $i in 1 to 10
    return { concat("Square of ", $i) : $i * $i }
|}


Took: 0.006300926208496094 ms
{"Square of 1": 1, "Square of 2": 4, "Square of 3": 9, "Square of 4": 16, "Square of 5": 25, "Square of 6": 36, "Square of 7": 49, "Square of 8": 64, "Square of 9": 81, "Square of 10": 100}

JSON Navigation

Up to now, you have learnt how to compose expressions so as to do some computations and to build objects and arrays. It also works the other way round: if you have some JSON data, you can access it and navigate. All you need to know is: JSONiq views an array as an ordered list of values, an object as a set of name/value pairs

Objects

You can use the dot operator to retrieve the value associated with a key. Quotes are optional, except if the key has special characters such as spaces. It will return the value associated thereto:


In [82]:
%%rumble
let $person := {
    "first name" : "Sarah",
    "age" : 13,
    "gender" : "female",
    "friends" : [ "Jim", "Mary", "Jennifer"]
}
return $person."first name"


Took: 0.009386062622070312 ms
"Sarah"

You can also ask for all keys in an object:


In [83]:
%%rumble
let $person := {
    "name" : "Sarah",
    "age" : 13,
    "gender" : "female",
    "friends" : [ "Jim", "Mary", "Jennifer"]
}
return { "keys" : [ keys($person)] }


Took: 0.00790095329284668 ms
{"keys": ["name", "age", "gender", "friends"]}

Arrays

The [[]] operator retrieves the entry at the given position:


In [84]:
%%rumble
let $friends := [ "Jim", "Mary", "Jennifer"]
return $friends[[1+1]]


Took: 0.00620579719543457 ms
"Mary"

It is also possible to get the size of an array:


In [85]:
%%rumble
let $person := {
    "name" : "Sarah",
    "age" : 13,
    "gender" : "female",
    "friends" : [ "Jim", "Mary", "Jennifer"]
}
return { "how many friends" : size($person.friends) }


Took: 0.006299018859863281 ms
{"how many friends": 3}

Finally, the [] operator returns all elements in an array, as a sequence:


In [86]:
%%rumble
let $person := {
    "name" : "Sarah",
    "age" : 13,
    "gender" : "female",
    "friends" : [ "Jim", "Mary", "Jennifer"]
}
return $person.friends[]


Took: 0.0063228607177734375 ms
"Jim"
"Mary"
"Jennifer"

Relational Algebra

Do you remember SQL's SELECT FROM WHERE statements? JSONiq inherits selection, projection and join capability from XQuery, too.


In [87]:
%%rumble
let $stores :=
[
    { "store number" : 1, "state" : "MA" },
    { "store number" : 2, "state" : "MA" },
    { "store number" : 3, "state" : "CA" },
    { "store number" : 4, "state" : "CA" }
]
let $sales := [
    { "product" : "broiler", "store number" : 1, "quantity" : 20  },
    { "product" : "toaster", "store number" : 2, "quantity" : 100 },
    { "product" : "toaster", "store number" : 2, "quantity" : 50 },
    { "product" : "toaster", "store number" : 3, "quantity" : 50 },
    { "product" : "blender", "store number" : 3, "quantity" : 100 },
    { "product" : "blender", "store number" : 3, "quantity" : 150 },
    { "product" : "socks", "store number" : 1, "quantity" : 500 },
    { "product" : "socks", "store number" : 2, "quantity" : 10 },
    { "product" : "shirt", "store number" : 3, "quantity" : 10 }
]
let $join :=
    for $store in $stores[], $sale in $sales[]
    where $store."store number" = $sale."store number"
    return {
        "nb" : $store."store number",
        "state" : $store.state,
        "sold" : $sale.product
    }
return [$join]


Took: 0.01275491714477539 ms
[{"nb": 1, "state": "MA", "sold": "broiler"}, {"nb": 1, "state": "MA", "sold": "socks"}, {"nb": 2, "state": "MA", "sold": "toaster"}, {"nb": 2, "state": "MA", "sold": "toaster"}, {"nb": 2, "state": "MA", "sold": "socks"}, {"nb": 3, "state": "CA", "sold": "toaster"}, {"nb": 3, "state": "CA", "sold": "blender"}, {"nb": 3, "state": "CA", "sold": "blender"}, {"nb": 3, "state": "CA", "sold": "shirt"}]

Access external data

Many implementations support collections of (and indices on) JSON objects or arrays. Rumble reads input through json-file(), parquet-file(), json-doc(), text-file(), csv-file() as well as structured-json-file().


In [44]:
%%rumble
json-file("put the path to a JSON lines file here")


Took: 0.010746955871582031 ms
Out[44]:
'Error [err: FODC0002]LINE:1:COLUMN:0:Malformed URL: put the path to a JSON lines file here'

In [45]:
%%rumble
json-doc("put the path to a small JSON file with an object spread over multiple lines here")


Took: 0.0070648193359375 ms
Out[45]:
'Error [err: FODC0002]LINE:1:COLUMN:0:File put the path to a small JSON file with an object spread over multiple lines here not found.'

It is also possible to get JSON content with an HTTP request, or by parsing it from a string. The EXPath http-client module (described in the Zorba documentation) allows you to make HTTP requests, and the jn:parse-json() function allows you to use the body as an object or an array. Rumble does not support HTTP requests (from JSONiq) yet.

Additional features

JSONiq supports JSON updates. You can declaratively update your JSON data. JSONiq provides updating expressions. The list of updates that is eventually output by your program is then applied to your JSON data.

    copy $people := {
      "John" : { "status" : "single" },
      "Mary" : { "status" : "single" } }
    modify (replace value of json $people.John.status with "married",
            replace value of json $people.Mary.status with "married")
    return $people
    -> { "John" : { "status" : "married" }, "Mary" : { "status" : "married" } }

Other updates are insertion into an object or an array, replacement of a value in an object or an array, deletion in an object or an array, renaming an object pair, appending to an array. This is documented on jsoniq.org.

Updates are not supported by Rumble yet, but Zorba supports them.

Here are a couple of more highlights:

  • JSONiq is a strongly typed language, but is smart enough to not bother you with types when unnecessary. It potentially supports static typing as well to detect errors before you even execute your program.
  • You can define your own functions and modules.
  • JSONiq provides you with loads of available modules shipped with Zorba.
  • JSONiq has tons of further features such as switch, typeswitch and try-catch expressions, universal/existential quantifiers, path expressions, filtering expressions, functors, mappings, grouping, windowing.
  • JSONiq supports XML. Yes: you can manipulate JSON and XML with the same language. JSONiq also exists in a different flavour that is a superset of XQuery, a W3C standard. JSONiq extends its data model to support JSON.
  • JSONiq supports scripting. If you need to write a full-fledged, side-effecting Web application, scripting is for you.
  • JSONiq supports Machine Learning with the brand new Rumble Machine Learning library.

The complete JSONiq specification is available on (http://www.jsoniq.org/)[http://www.jsoniq.org/docs/JSONiq/webhelp/index.html]


In [ ]: