json
Parse JSON text into structs and values, write it back out, and read huge files one token at a time.
import "std:json"std:json reads and writes JSON, for web APIs, config files and data another program will read. It turns text into a typed struct, into plain objects and arrays, or into a stream of tokens when the file is too big to hold in memory.
import "std:json"Parse into a struct#
The best way to read JSON is to say what shape you expect. json.parse<T> checks the text against the type T and returns a result: the value if it fits, an error if it doesn't.
import "std:json"
struct User (
string name = ""
int age = 0
string[] tags = []
)
User u = json.parse<User>(`{"name": "Grace", "age": 85, "tags": ["navy", "compilers"]}`).unwrap()
log u.name u.age u.tags
log u.tags.lenKeys the struct doesn't declare are ignored, and keys missing from the JSON keep the field's default. Nested structs and arrays of structs work the same way.
import "std:json"
struct Address (
string city = ""
string country = "unknown"
)
struct Person (
string name = ""
Address home = Address()
Address[] visited = []
)
string text = `{
"name": "Ada",
"home": {"city": "London"},
"visited": [{"city": "Turin", "country": "Italy"}, {"city": "Paris"}],
"extra": true
}`
Person p = json.parse<Person>(text).unwrap()
log p.home.city p.home.country
for v in p.visited ( log v.city "-" v.country )Try it: add a boolean admin = false field to User and an "admin": true key to the text, then log u.admin.
When the JSON does not fit#
A wrong type is an error that names the struct and the field. Invalid JSON is a ParseError that says where it broke. Handle both with switch or catch:
import "std:json"
struct User (
string name = ""
int age = 0
)
string[] inputs = [`{"name": "Bob", "age": "old"}`, `{"name": "Bob",}`, `[1, 2]`, ``]
for text in inputs (
switch json.parse<User>(text) (
case ok(u)
log "ok" u.name
case err(e)
log e.kind ":" e.message
)
)Parsing is strict. Duplicate keys, trailing commas, numbers out of range and anything after the document are all errors, so a document that parses is one you can trust.
Try it: change "old" to 30 and see the first input succeed.
Parse into plain values#
When you don't know the shape in advance, leave out the type. json.parse(text) gives objects, arrays, strings, numbers, booleans and null as OSL values.
import "std:json"
auto doc = json.parse(`{"id": 7, "tags": ["a", "b"], "meta": {"ok": true}}`).unwrap()
log doc.id
log doc.tags[1]
log doc.meta.ok
log doc.missingPass {type: "object"} or {type: "array"} to insist on the kind of root value. isValid, isObject and isArray answer yes or no without building anything, which is handy for checking input before you route it.
import "std:json"
log json.isValid(`{"a": 1}`) json.isValid("{a: 1}")
log json.isObject(`{"a": 1}`) json.isArray(`{"a": 1}`)
log json.parse("[1, 2]", {type: "object"}).isErr()Write JSON#
json.stringify turns any value into JSON text. Object keys come out in sorted order, so the same data always gives the same text. That makes the output safe to compare, diff and commit.
import "std:json"
struct Point (
int x = 0
int y = 0
)
log json.stringify({name: "osl", version: 9, tags: ["go", "fast"], ok: true})
log json.stringify(Point(3, 4))
log json.stringify("a \"quoted\" line\n")
log json.stringify([1, 2.5, null])Give it indent for text people will read:
import "std:json"
auto data = {name: "osl", tags: ["go", "fast"], nested: {a: 1}}
log json.stringify(data, {indent: 2})Structs keep their fields in declaration order; plain objects are sorted. Values JSON can't hold, such as NaN, infinity or a function, throw instead of writing something wrong.
Try it: add a nested object to the first value and watch the keys stay sorted.
Round trips#
Parse and stringify are inverses for data that fits a struct. A common pattern is to read a typed value, change it and write it back:
import "std:json"
struct Settings (
string theme = "light"
int fontSize = 12
)
Settings s = json.parse<Settings>(`{"theme": "dark"}`).unwrap()
s.fontSize += 2
log json.stringify(s)
Settings again = json.parse<Settings>(json.stringify(s)).unwrap()
log again.theme again.fontSizeFiles#
json.parseFile(path) reads and parses a file in one step, with the same options as parse. Write the file with fs. A missing file and an invalid file are different errors, so you can tell the user which one went wrong.
import "std:json"
import "std:fs"
string pretty = json.stringify({port: 8080, debug: false}, {indent: 2})
fs.writeFile("config.json", pretty).unwrap()
fs.writeFile("broken.json", "{\"port\": ").unwrap()
auto config = json.parseFile("config.json").unwrap()
log config.port
for path in ["missing.json", "broken.json"] (
switch json.parseFile(path) (
case ok(v)
log "loaded" path
case err(e)
log path e.kind
)
)
fs.remove("config.json").unwrap()
fs.remove("broken.json").unwrap()To get a typed value from a file, read the text and use parse<T>:
import "std:json"
import "std:fs"
struct Config (
int port = 80
boolean debug = false
)
fs.writeFile("typed.json", `{"port": 9000}`).unwrap()
Config c = json.parse<Config>(fs.readFile("typed.json").unwrap()).unwrap()
log c.port c.debugTry it: change broken.json into valid JSON and see it load.
Streaming large files#
parse builds the whole document in memory. For a file of hundreds of megabytes, json.open gives you a stream that reads one token at a time. events() yields records with a type, a value and the nesting depth.
import "std:json"
import "std:fs"
fs.writeFile("small.json", `{"name": "osl", "tags": ["a", "b"], "n": 3}`).unwrap()
auto stream = json.open("small.json").unwrap()
for ev in stream.events() (
log ev.depth ev.type ev.value
)
stream.close().unwrap()The usual job is to look only at the parts you care about. This one adds up the id of every record in a file and steps over each payload with skip(), so the bulky parts are never decoded. After a key event, skip() skips that key's value.
import "std:json"
import "std:fs"
string[] rows = []
for i in 1 to 5 ( rows.append(`{"id": ${i}, "payload": {"deep": [1, 2, 3]}}`) )
fs.writeFile("rows.json", "[" ++ rows.join(",") ++ "]").unwrap()
auto s = json.open("rows.json", {maxBytes: 1000000}).unwrap()
int total = 0
int records = 0
for ev in s.events() (
if ev.type == "object-start" ( records++ )
if ev.type == "key" and ev.value == "payload" (
s.skip().unwrap()
)
if ev.type == "number" ( total += ev.value )
)
s.close().unwrap()
log records "records, ids add up to" totalreadObject() reads the next object whose values are plain strings, numbers, booleans or null, and returns it directly:
import "std:json"
import "std:fs"
fs.writeFile("flat.json", `{"a": 1, "b": "two", "c": null}`).unwrap()
auto s = json.open("flat.json").unwrap()
auto obj = s.readObject().unwrap()
log obj.a obj.b obj.c
s.close().unwrap()The maxBytes option of open and the maxKeys option of readObject refuse input bigger than you are willing to handle, which matters when the file comes from someone else.
Summarise a config
Parse the JSON text into a Config struct. Print the app name, then each server on its own line as host:port. If a server has no port, use 80. Finally print how many servers there are.
shop a.example:8080 b.example:80 2
A struct has int age = 0. What does json.parse<User> do with {"name": "Bo"}, which has no age?
Which of these are objects?
Print object for each entry that is a valid JSON object and other for the rest.
object other other object
API reference#
20 members, generated from the std:json source of OSL 9.3.0.
Functions
json.format(any data) stringDeprecated. Deprecated: use json.stringify(value, {indent: 2}) instead.
format encodes data as JSON indented by two spaces, or "" when it can't be encoded.
json.isArray(string text) booleanisArray reports whether text is a JSON document whose root is an array.
json.isObject(string text) booleanisObject reports whether text is a JSON document whose root is an object.
json.isValid(string text) booleanisValid reports whether text is a JSON document json.parse accepts.
json.open(string path, any ...opts) result<*json.Stream, error>open opens the JSON file at path for reading one token at a time, so a large document never has to fit in memory. Read it with stream.events(), stream.skip() and stream.readObject(), and close it when done. Options: maxBytes (refuse files larger than this many bytes; no limit by default). Fails with NotFoundError, PermissionError or IOError when the file can't be opened, and ValueError when it is larger than maxBytes. Passing a number instead of options is the deprecated form of {maxBytes: n}.
json.parse(any text, object? options) result<any, error>parse reads a JSON document from text: objects, arrays, strings, numbers, booleans and null. Fails with ParseError, naming the line and column, for malformed JSON, duplicate object keys, numbers out of range and trailing data. Options: type ("object", "array" or "any", the default) also fails with ParseError when the document is a different kind of value. Use json.parse<T>(text) to check the result against an OSL type.
json.parseArray(any data) resultDeprecated. Deprecated: use json.parse(text, {type: "array"}) instead.
parseArray parses text and requires a JSON array.
json.parseFile(string path, object? options) result<any, error>parseFile reads and parses the JSON file at path, with the same options as json.parse. Fails with NotFoundError, PermissionError or IOError when the file can't be read and ParseError when it isn't valid JSON.
json.parseObject(any data) resultDeprecated. Deprecated: use json.parse(text, {type: "object"}) instead.
parseObject parses text and requires a JSON object.
json.stringify(any value, object? options) stringstringify encodes value as JSON text, with object keys in sorted order. Options: indent (spaces per nesting level, default 0 for compact output). Throws ValueError for NaN, infinite numbers and cyclic values, and TypeError for values JSON can't represent, such as functions.
*json.Stream
stream.close() result<unit, error>close closes the file. Closing twice is a no-op, and every other method throws StateError afterwards. Fails with IOError if the file can't be closed cleanly.
stream.error() stringDeprecated. Deprecated: errors are now thrown by stream.events() and returned by stream.skip() and stream.readObject().
error returns the stream's failure message, or "".
stream.events() iter<{depth: int, type: string, value: any}>events iterates over the rest of the document one token at a time, as records {type, value, depth} (see json.Event). The loop shares the stream's position, so calling skip() or readObject() inside it consumes those tokens. Throws ParseError, naming the line and column, when the file turns out to be malformed; wrap the loop in try() to handle it. Throws StateError once the stream is closed.
stream.more() booleanDeprecated. Deprecated: use for event in stream.events() instead.
more reports whether another event can be read.
stream.next() objectDeprecated. Deprecated: use for event in stream.events() instead.
next reads the next event, or {type: "eof"} at the end.
stream.ok() booleanDeprecated. Deprecated: errors are now thrown by stream.events() and returned by stream.skip() and stream.readObject().
ok reports whether the stream has not failed.
stream.readObject(object? options) result<object, error>readObject reads the next value, which must be an object whose values are all strings, numbers, booleans or null, and returns it. Options: maxKeys (fail when the object has more keys; no limit by default) and stringsOnly (require every value to be a string; default false). Fails with ParseError when the next value is not such an object or the JSON is malformed, and ValueError when it has more than maxKeys keys. Throws StateError once the stream is closed.
stream.readScalarMap(any maxValues) resultDeprecated. Deprecated: use stream.readObject({maxKeys: n}) instead.
readScalarMap reads the next value as an object of at most maxValues scalar values.
stream.readStringMap(any maxValues) resultDeprecated. Deprecated: use stream.readObject({maxKeys: n, stringsOnly: true}) instead.
readStringMap reads the next value as an object of at most maxValues string values.
stream.skip() result<boolean, error>skip skips the next value, however deeply nested, without decoding it, and reports whether there was one: false at the end of the document or when the next token closes the enclosing object or array. After a "key" event it skips that key's value. Fails with ParseError for malformed JSON and throws StateError once the stream is closed.