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 }'
```
|