Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Read and write JSON

std.json decodes and encodes JSON under explicit limits. It offers two levels: a value tree for documents that fit in memory, and events for documents that should not be held whole.

Parse a document into a tree

import std.json
from std.json import Json

def main() -> Result[(), AllocError | IoError | std.json.JsonError]:
    bounds = std.json.limits()
    value = std.json.parse_text("{\"name\": \"Ada\", \"tags\": [\"x\", \"y\"], \"age\": 36, \"ratio\": 0.5}", &bounds)?
    match &value:
        case Json.Object(members):
            for member in members:
                match &member.value:
                    case Json.Text(text):
                        print(member.key, "is the text", text)?
                    case Json.Int(number):
                        print(member.key, "is the integer", *number)?
                    case Json.Float(number):
                        print(member.key, "is the float", *number)?
                    case Json.Array(items):
                        print(member.key, "has", len(items), "items")?
                    case _:
                        print(member.key, "is something else")?
            print(std.json.find(members, "age"), std.json.find(members, "email"))?
        case _:
            print("not an object")?
    print(std.json.encode_text(&value, &bounds)?)?
    Ok(())
name is the text Ada
tags has 2 items
age is the integer 36
ratio is the float 0.5
Option[i64].Some(2) Option[i64].Nothing
{"name":"Ada","tags":["x","y"],"age":36,"ratio":0.5}

parse_text returns a Json value, an enum with one variant per JSON type. match is how you find out which one you have. A number written without a fraction or exponent is Json.Int; any other number is Json.Float.

An object is a list[Member] in source order, where each member has a key and a value. find returns the index of the first member with a key. Output is compact: no spaces or line breaks, and members stay in the order of the list.

Every call takes Limits. limits() gives the defaults: 64 levels of nesting, 64 MiB of input, 64 MiB of output, and duplicate keys rejected.

Errors say where

A JsonError carries a fault, the byte offset, and a line and column that both start at 1.

import std.json
from std.json import Limits

def attempt(text: &str, bounds: &Limits) -> Result[(), AllocError | IoError | std.json.JsonError]:
    match std.json.parse_text(text, bounds):
        case Ok(value):
            print("ok", std.json.encode_text(&value, bounds)?)?
        case Err(error):
            print(error.fault, "at byte", error.offset, "line", error.line, "column", error.column)?
    Ok(())

def main() -> Result[(), AllocError | IoError | std.json.JsonError]:
    mut bounds = std.json.limits()
    attempt("[1, 2,\n  oops]", &bounds)?
    attempt("[1, 2", &bounds)?
    attempt("{\"a\": 1, \"a\": 2}", &bounds)?
    attempt("[9223372036854775808]", &bounds)?
    attempt("[1] [2]", &bounds)?
    bounds.max_depth = 2
    attempt("[[1]]", &bounds)?
    attempt("[[[1]]]", &bounds)?
    Ok(())
std.json.JsonFault.UnexpectedByte at byte 9 line 2 column 3
std.json.JsonFault.UnexpectedEnd at byte 5 line 1 column 6
std.json.JsonFault.DuplicateKey at byte 9 line 1 column 10
std.json.JsonFault.NumberOutOfRange at byte 1 line 1 column 2
std.json.JsonFault.TrailingData at byte 4 line 1 column 5
ok [[1]]
std.json.JsonFault.DepthExceeded at byte 2 line 1 column 3

The decoder accepts standard JSON and nothing else: no comments, no trailing commas, one document per input. An integer that does not fit i64 is an error in the tree, never a rounded float. max_depth counts open arrays and objects, so the limit of 2 accepts [[1]] and stops at the third [.

Stream events instead of building a tree

A Decoder reports one Event at a time, and an Encoder accepts one call per event. Neither holds the document, so memory stays bounded however long the input is.

import std.bytes
import std.io
import std.json
from std.json import Event

def main() -> Result[(), IoError | AllocError | ParseError | std.json.JsonError | std.bytes.BytesError | OverflowError[i64]]:
    bounds = std.json.limits()
    mut reader = std.io.cursor(std.bytes.from_text("[{\"id\": 7, \"ok\": true}, {\"id\": 18446744073709551615}]")?)
    mut decoder = std.json.decoder(&bounds)?
    mut ids = 0
    mut expecting_id = False
    while True:
        event = std.json.read_event(&mut decoder, &mut reader)?
        match &event:
            case Event.End:
                break
            case Event.Key(name):
                expecting_id = name == "id"
            case Event.Number(text):
                if expecting_id:
                    ids += 1
                    print("id", u64.parse(text)?)?
            case _:
                pass
    print(ids, "ids in", decoder.offset(), "bytes")?

    mut encoder = std.json.encoder(&bounds)?
    encoder.start_object()?
    encoder.key("ids")?
    encoder.integer(ids)?
    encoder.key("note")?
    encoder.text("line one\nline two")?
    encoder.end_object()?
    mut out = std.io.cursor(std.bytes.empty()?)
    std.json.write_pending(&mut encoder, &mut out)?
    print(out.data.to_text()?)?
    match encoder.null():
        case Ok(_):
            print("written")?
        case Err(error):
            print(error.fault)?
    Ok(())
id 7
id 18446744073709551615
2 ids in 53 bytes
{"ids":2,"note":"line one\nline two"}
std.json.JsonFault.InvalidState

read_event pulls bytes from any std.io reader as the decoder needs them and returns Event.End after the document. Event.Number holds the number’s text exactly as written, so the program chooses the type: here u64, which holds a value the tree’s i64 cannot.

The encoder checks call order. A key outside an object, a value where a key belongs, or a second top-level value is InvalidState, so an encoder cannot produce malformed JSON. write_pending hands the bytes produced so far to a writer; call it between events to keep the buffer small.

The std.json reference lists every fault, limit, and cost.