File: json.md

package info (click to toggle)
liquidsoap 2.3.2-2
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 11,912 kB
  • sloc: ml: 67,867; javascript: 24,842; ansic: 273; xml: 114; sh: 96; lisp: 96; makefile: 26
file content (307 lines) | stat: -rw-r--r-- 8,879 bytes parent folder | download
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
## Importing JSON values

_Note:_ If you are reading this page for the first time, you might want to skip directly to the
explicit type annotation below as this is the recommended way of parsing JSON data. The content
before that is here to explain the inner workings of JSON parsing in `liquidsoap`.

Liquidsoap supports importing JSON values through a special `let` syntax. Using this syntax
makes it relatively natural to parse JSON data in your script while keeping type-safety at runtime.
Here's an example:

```{.liquidsoap include="json1.liq"}

```

This prints:

```
We parsed a JSON object and got value abc for attribute foo!
```

What happened here is that liquidsoap kept track of the fact that `v` was called with
`v.foo` and that the result of that was a string. Then, at runtime, it checks the parsed
JSON value against this type and raises an issue if that did not match. For instance,
the following script:

```liquidsoap
let json.parse v = '{"foo": 123}'

print("We parsed a JSON object and got value " ^ v.foo ^ " for attribute foo!")
```

raises the following exception:

```
Error 14: Uncaught runtime error:
type: json,
message: "Parsing error: json value cannot be parsed as type {foo: string, _}"
```

Of course, this all seems pretty trivial presented like that but, let's switch to reading a file instead:

```liquidsoap
let json.parse v = file.contents("/path/to/file.json")

print("We parsed a JSON object and got value " ^ v.foo ^ " for attribute foo!")
```

Now, this is getting somewhere! Let's push it further and parse a whole `package.json` from
a typical `npm` package:

```liquidsoap
# Content of package.json is:
# {
#  "name": "my_package",
#  "version": "1.0.0",
#  "scripts": {
#    "test": "echo \"Error: no test specified\" && exit 1"
#  },
#  ...
let json.parse package = file.contents("/path/to/package.json")

name = package.name
version = package.version
test = package.scripts.test

print("This is package " ^  name ^ ", version " ^ version ^ " with test script: " ^ test)
```

And we get:

```
This is package my_package, version 1.0.0 with test script: echo "Error: no test specified" && exit 1
```

This can even be combined with _patterns_:

```liquidsoap
let json.parse {
  name,
  version,
  scripts = {
    test
  }
} = file.contents("/path/to/package.json")

print("This is package " ^  name ^ ", version " ^ version ^ " with test script: " ^ test)
```

Now, this is looking nice!

## Explicit type annotation

Explicit type annotation are the recommended way to parse JSON data.

Let's try a slight variation of the previous script now:

```liquidsoap
let json.parse {
  name,
  version,
  scripts = {
    test
  }
} = file.contents("/path/to/package.json")

print("This is package #{name}, version #{version} with test script: #{test}")
```

This returns:

```
This is package null, version null with test script: null
```

What? 🤔

This is because, in this script, we only use `name`, `version`, etc.. through the interpolation syntax `#{...}`. However, interpolated
variables can be anything so this does not leave enough information to the typing system to know what type those variables should be and,
in this case, we default to `null`.

In order to avoid bad surprises like this, it is usually recommended to add **type annotations** to your json parsing call
to explicitly state what kind of data you are expecting. Let's add one here:

```liquidsoap
let json.parse ({
  name,
  version,
  scripts = {
    test
  }
} : {
  name: string,
  version: string,
  scripts: {
    test: string
  }
}) = file.contents("/path/to/package.json")

print("This is package #{name}, version #{version} with test script: #{test}")
```

And we get:

```
This is package my_package, version 1.0.0 with test script: echo "Error: no test specified" && exit 1
```

Back to normal!

### Type syntax

The syntax for type annotation is as follows:

#### Ground types

`string`, `int`, `float` are parsed as, resp., a string, an integer or a floating point number. Note that if your json value contains an integer such as `123`, parsing it as a floating point number will succeed. Also, if an integer is too big to be represented as an `int` internally, it will be parsed as a floating point number.

#### Nullable types

All type annotation can be postfixed with a trailing `?` to denote a _nullable_ value. If a type is nullable, the json parser will return `null` when it cannot parse
the value as the principal type. This is particularly useful when you are not sure of all the types that you are parsing.

For instance, some `npm` packages do not have a `scripts` entry or a `test` entry, so you would parse them as:

```liquidsoap
let json.parse ({
  name,
  version,
  scripts,
} : {
  name: string,
  version: string,
  scripts: {
    test: string?
  }?
}) = file.contents("/path/to/package.json")
```

And, later, inspect the returned value to see if it is in fact present. You can do it in several ways:

```liquidsoap
# Check if the value is defined:
test =
  if null.defined(scripts) then
    null.get(scripts.test)
  else
    null ()
  end

# Use the ?? syntax:
test = (scripts ?? { test = null() }).test
```

#### Tuple types

The type `(int * float * string)` tells liquidsoap to parse a JSON array whose _first values_ are of type: `int`, `float` and `string`. If any further values
are present in the array, they will be ignored.

For arrays as well as any other structured types, the special notation `_` can be used to denote any type. For instance, `(_ * _ * float)` denotes an JSON
array whose first 2 elements can be of any type and its third element is a floating point number.

#### Lists

The type `[int]` tells liquidsoap to parse a JSON array where _all its values_ are integers as a list of integers. If you are not sure if all elements in the
array are integers, you can always use nullable integers: `[int?]`

#### Objects

The type `{foo: int}` tells liquidsoap to parse a JSON object as a record with an attribute labelled `foo` whose value is an integer. All other
attributes are ignored.

Arbitrary object keys can be parsed using the following syntax: `{"foo bar key" as foo_bar_key: int}`, which tells liquidsoap to parse a JSON object
as a record with an attribute labelled `foo_bar_key` which maps to the attribute `"foo bar key"` from the JSON object.

#### Associative lists as objects

It can sometimes be useful to parse a JSON object as an associative list, for instance if you do not know in advance all the possible keys of
an object. In this case, you can use the special type: `[(string * int)] as json.object`. This tells liquidsoap to parse the JSON object as a list
of pairs `(string * int)` where `string` represents the attribute label and `int` represent the attribute value.

If you are not sure if all the object values are integers you can always use nullable integers: `[(string * int?)] as json.object`

### Parsing errors

When parsing fails, a `error.json` is raised which can be caught at runtime:

```liquidsoap
try
   let json.parse ({
      status,
      data = {
        track
      }
    } : {
      status: string,
      data: {
        track: string
      }
    }) = res

    # Do something on success here..
catch err: [error.json] do
  # Do something on parse errors here..
end
```

#### Example

Here's a full example. Feel free to refer to `tests/language/json.liq` in the source code for more of them.

```{.liquidsoap include="json-ex.liq"}

```

It returns

```
  - x : {
    foo = 34.24,
    gni_gno = true,
    nested = {
      tuple = (null, 3.14),
      list = [44., 55., 66.12],
      nullable_list = [null, 23, null],
      object_as_list = [("foo", 123.), ("gni", 456.0), ("gno", 3.14)],
      arbitrary_object_key = true,
      not_present = null
    }
  }
```

### JSON5 extension

Liquidsoap supports the [JSON5](https://json5.org/) extension. Parsing of `json5` values is enabled with the following argument:

```liquidsoap
let json.parse[json5=true] x = ...
```

If a `json5` variable is in scope, you can also simply use `let json.parse[json5] x = ...`

## Exporting JSON values

Exporting JSON values can be done using the `json.stringify` function:

```{.liquidsoap include="json-stringify.liq"}

```

Please note that not all values are exportable as JSON, for instance function. In such cases the function will raise an `error.json` exception.

## Generic JSON objects

Generic `JSON` objects can be manipulated through the `json()` operator. This operator
returns an opaque json variable with methods to `add` and `remove` attributes:

```liquidsoap
j = json()
j.add("foo", 1)
j.add("bla", "bar")
j.add("baz", 3.14)
j.add("key_with_methods", "value".{method = 123})
j.add("record", { a = 1, b = "ert"})
j.remove("foo")
s = json.stringify(j)
- s: '{ "record": { "b": "ert", "a": 1 }, "key_with_methods": "value", "bla": "bar", "baz": 3.14 }'
```