Packages

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.

osl
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.

main.osl
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.len
Click the code to edit it

Keys 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.

main.osl
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 )
Click the code to edit it

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:

main.osl
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
  )
)
Click the code to edit it

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.

main.osl
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.missing
Click the code to edit it

Pass {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.

main.osl
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()
Click the code to edit it

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.

main.osl
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])
Click the code to edit it

Give it indent for text people will read:

main.osl
import "std:json"

auto data = {name: "osl", tags: ["go", "fast"], nested: {a: 1}}
log json.stringify(data, {indent: 2})
Click the code to edit it

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:

main.osl
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.fontSize
Click the code to edit it

Files#

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.

main.osl
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()
Click the code to edit it

To get a typed value from a file, read the text and use parse<T>:

main.osl
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.debug
Click the code to edit it

Try 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.

main.osl
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()
Click the code to edit it

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.

main.osl
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" total
Click the code to edit it

readObject() reads the next object whose values are plain strings, numbers, booleans or null, and returns it directly:

main.osl
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()
Click the code to edit it

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.

Your turn

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.

main.osl
Ctrl Enter runs
Expected output
shop
a.example:8080
b.example:80
2
Quick check

A struct has int age = 0. What does json.parse<User> do with {"name": "Bo"}, which has no age?

Your turn

Which of these are objects?

Print object for each entry that is a valid JSON object and other for the rest.

main.osl
Ctrl Enter runs
Expected output
object
other
other
object

API reference#

20 members, generated from the std:json source of OSL 9.3.0.

Functions

#json.format(any data) string

Deprecated. 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) boolean

isArray reports whether text is a JSON document whose root is an array.

#json.isObject(string text) boolean

isObject reports whether text is a JSON document whose root is an object.

#json.isValid(string text) boolean

isValid 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) result

Deprecated. 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) result

Deprecated. Deprecated: use json.parse(text, {type: "object"}) instead.

parseObject parses text and requires a JSON object.

#json.stringify(any value, object? options) string

stringify 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() string

Deprecated. 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() boolean

Deprecated. Deprecated: use for event in stream.events() instead.

more reports whether another event can be read.

#stream.next() object

Deprecated. Deprecated: use for event in stream.events() instead.

next reads the next event, or {type: "eof"} at the end.

#stream.ok() boolean

Deprecated. 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) result

Deprecated. 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) result

Deprecated. 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.