Chapter 21 16 min read

JSON

A small JSON parser and emitter built around one tagged-union type, JsonValue. UTF-8 in, UTF-8 out. Build trees with static constructors, mutate them with obj_set / arr_push, read them back with get / at / the typed get_* / opt_* accessors, and round-trip with JsonValue::parse / emit.

Import

import stdlib::json::*;

Public surface at a glance

Item Kind Summary
JSON_NULLJSON_OBJECT pub const i32 Kind tags, 06.
JsonValue pub struct The tagged-union value node.
JsonParser pub struct Parser cursor (rarely constructed directly).
JsonBind pub trait Two-way struct ⇄ JSON binding.
null/bool/int/float/string/array/object static ctors Allocate a fresh *JsonValue.
parse/emit/escape parse/serialise Document ⇄ string.
arr_push/obj_set mutators Build arrays/objects in place.
get/at/len navigation Untyped child access.
as_string/as_int/as_float/as_bool/is_null scalar accessors Coercing reads, never fail.
get_string/get_int/get_bool/get_float/get_object/get_array typed (-> !T) Type-checked required reads.
opt_string/opt_int/opt_bool/opt_float typed (-> ?T) Type-checked optional reads.
get_any typed Pass-through of any kind.

Kind constants

Every JsonValue carries a kind: i32 tag drawn from these pub const values.

Constant Value JSON type
JSON_NULL 0 null
JSON_BOOL 1 true / false
JSON_INT 2 integer number
JSON_FLOAT 3 floating-point number
JSON_STRING 4 string
JSON_ARRAY 5 array
JSON_OBJECT 6 object
let v: *JsonValue = JsonValue::parse("[1,2,3]");
if v.kind == JSON_ARRAY { println!("array of", v.len()); }

The JsonValue struct

pub struct JsonValue {
    pub kind: i32,
    pub b: bool,
    pub i: i32,
    pub f: f64,
    pub s: string,
    pub arr: *Vector<JsonValue>,
    pub obj_keys: *Vector<string>,
    pub obj_vals: *Vector<JsonValue>,
}

Only the field matching kind is meaningful: b for JSON_BOOL, i for JSON_INT, f for JSON_FLOAT, s for JSON_STRING, arr for JSON_ARRAY, and the parallel obj_keys / obj_vals vectors for JSON_OBJECT. Prefer the accessor methods below over touching fields directly; the typed accessors enforce the right kind.

Constructors (static)

Each constructor allocates a fresh heap *JsonValue.

Function Signature Description
null fn null() -> *JsonValue JSON null.
bool fn bool(b: bool) -> *JsonValue JSON boolean.
int fn int(n: i32) -> *JsonValue JSON integer (stays an int until parsed back as a float).
float fn float(f: f64) -> *JsonValue JSON float; always emits as a float, even for whole values.
string fn string(s: string) -> *JsonValue JSON string; escaped at emit time, so pass raw text.
array fn array() -> *JsonValue Empty array; fill with arr_push.
object fn object() -> *JsonValue Empty object; fill with obj_set.
println!(JsonValue::null().emit());          // null
println!(JsonValue::bool(true).emit());      // true
println!(JsonValue::int(42).emit());         // 42
println!(JsonValue::float(3.14).emit());     // 3.140000
println!(JsonValue::string("hi").emit());    // "hi"

Parsing and serialising

parse

pub fn parse(src: string) -> *JsonValue

Parses a JSON document into a tree. The parser is lenient: malformed input may produce a partially populated tree rather than an error (there is no !T result). Validate the resulting kind if the input is untrusted.

let v: *JsonValue = JsonValue::parse("{\"name\":\"alice\",\"age\":30}");
println!(v.get("name").as_string());   // alice
println!(v.get("age").as_int());       // 30

Lenient recovery in practice: truncated objects keep the keys parsed so far (a value with no following token becomes null), and input that is not JSON at all yields a JSON_NULL node.

import stdlib::json::*;

fn main() -> i32 {
    let v: *JsonValue = JsonValue::parse("{\"a\":1,\"b\":");
    println!(v.kind == JSON_OBJECT);   // true
    println!(v.len());                 // 2  (b -> null)

    let bad: *JsonValue = JsonValue::parse("not json");
    println!(bad.kind == JSON_NULL);   // true
    return 0;
}

emit

pub fn emit(self: *JsonValue) -> string

Serialises to a compact (no whitespace) string. The result is heap-owned by the caller. Floats render with format!'s default f64 formatting (e.g. 3.140000). A null pointer emits as "null".

let o: *JsonValue = JsonValue::object();
o.obj_set("x", JsonValue::int(1));
o.obj_set("y", JsonValue::array());
println!(o.emit());   // {"x":1,"y":[]}

escape

pub fn escape(s: string) -> string

Wraps s in double quotes and escapes JSON-special bytes — ", \, CR, LF, tab, and other control bytes (< 32, rendered as ?). This is the same routine emit uses for string values and object keys.

println!(JsonValue::escape("hi"));     // "hi"
println!(JsonValue::escape("a\nb"));   // "a\nb"

Building

arr_push

pub fn arr_push(self: *JsonValue, v: *JsonValue)

Appends v to a JSON array. No-op when self is not an array.

obj_set

pub fn obj_set(self: *JsonValue, key: string, v: *JsonValue)

Sets keyv on a JSON object. No-op when self is not an object.

let user: *JsonValue = JsonValue::object();
user.obj_set("name", JsonValue::string("alice"));
user.obj_set("age",  JsonValue::int(30));
println!(user.emit());   // {"name":"alice","age":30}

let a: *JsonValue = JsonValue::array();
a.arr_push(JsonValue::int(1));
a.arr_push(JsonValue::int(2));
println!(a.emit());      // [1,2]

Nesting just stores child *JsonValue nodes as values — there is no special API for nested arrays/objects, you obj_set / arr_push a freshly built child:

import stdlib::json::*;

fn main() -> i32 {
    let root: *JsonValue = JsonValue::object();
    root.obj_set("ok", JsonValue::bool(true));

    let nums: *JsonValue = JsonValue::array();
    nums.arr_push(JsonValue::int(1));
    nums.arr_push(JsonValue::int(2));
    nums.arr_push(JsonValue::float(2.5));
    root.obj_set("nums", nums);

    let addr: *JsonValue = JsonValue::object();
    addr.obj_set("city", JsonValue::string("Rio"));
    root.obj_set("addr", addr);

    root.obj_set("note", JsonValue::null());

    println!(root.emit());
    // {"ok":true,"nums":[1,2,2.500000],"addr":{"city":"Rio"},"note":null}
    return 0;
}
Method Signature Returns
get fn get(self: *JsonValue, key: string) -> *JsonValue The value for key, or null if absent / not an object.
at fn at(self: *JsonValue, idx: i32) -> *JsonValue The element at idx, or null if out of range / not an array.
len fn len(self: *JsonValue) -> i32 Array element count, object key count, or 0 for scalars / null.

get and at hand back a fresh heap copy of the child value, so it is safe to keep using after the parent goes out of scope. Both return a Glide null pointer on a miss — compare against null directly.

let v: *JsonValue = JsonValue::parse("{\"name\":\"alice\"}");
println!(v.get("name").as_string());   // alice
if v.get("nope") == null { println!("missing"); }

let arr: *JsonValue = JsonValue::parse("[10, 20, 30]");
println!(arr.at(1).as_int());          // 20
if arr.at(99) == null { println!("out of range"); }
println!(arr.len());                   // 3

Scalar accessors

These never fail — they coerce or fall back to a zero value when the kind does not match. Use them when you already know (or do not care about) the type.

Method Signature Fallback
as_string fn as_string(self: *JsonValue) -> string "" for non-strings / null.
as_int fn as_int(self: *JsonValue) -> i32 Floats truncate toward zero; non-numbers → 0.
as_float fn as_float(self: *JsonValue) -> f64 Ints coerce to f64; non-numbers → 0.0.
as_bool fn as_bool(self: *JsonValue) -> bool false for non-bools / null (no truthy coercion).
is_null fn is_null(self: *JsonValue) -> bool true for JSON null or a Glide null pointer.
println!(JsonValue::float(3.9).as_int());    // 3
println!(JsonValue::int(7).as_float());      // 7.0
println!(JsonValue::int(1).as_bool());       // false  (no truthy coercion)
println!(JsonValue::null().is_null());       // true

Typed object accessors

These read a named field with type checking. Each comes in a required form returning a Result (!T) and, for the scalars, an optional form returning an Option (?T).

Required fields (-> !T)

Method Signature Errors when
get_string fn get_string(self, key: string) -> !string key missing, value null, or not a string.
get_int fn get_int(self, key: string) -> !i32 key missing, value null, or not a JSON int (a JSON 3.0 is the wrong type).
get_bool fn get_bool(self, key: string) -> !bool key missing, value null, or not a bool.
get_float fn get_float(self, key: string) -> !f64 key missing or value null; a JSON int promotes to f64.
get_object fn get_object(self, key: string) -> !*JsonValue key missing, value null, or not an object.
get_array fn get_array(self, key: string) -> !*JsonValue key missing, value null, or not an array.

Calling any get_* on a non-object self returns err("not an object"). Missing keys produce missing key "<key>"; type mismatches produce field "<key>" is not <want>. Propagate with ? or inspect .ok / .err / .val.

import stdlib::json::*;

fn main() -> !i32 {
    let v: *JsonValue =
        JsonValue::parse("{\"name\":\"alice\",\"age\":30,\"active\":true,\"score\":3.14}");

    let name: string = v.get_string("name")?;
    let age:  i32    = v.get_int("age")?;
    let on:   bool   = v.get_bool("active")?;
    let sc:   f64    = v.get_float("score")?;
    println!(name, age, on, sc);

    let r: !i32 = v.get_int("missing");
    if !r.ok { println!(r.err); }   // missing key "missing"
    return ok(0);
}

get_object / get_array return the raw child *JsonValue so you can drill further. This is the idiomatic way to read nested documents:

import stdlib::json::*;

fn main() -> !i32 {
    let src: string =
        "{\"user\":{\"name\":\"alice\",\"age\":30,\"active\":true},\"tags\":[\"x\",\"y\",\"z\"]}";
    let v: *JsonValue = JsonValue::parse(src);

    let user: *JsonValue = v.get_object("user")?;
    let name: string = user.get_string("name")?;
    let age:  i32    = user.get_int("age")?;
    let on:   bool   = user.get_bool("active")?;
    println!(name, age, on);   // alice 30 true

    let tags: *JsonValue = v.get_array("tags")?;
    for let i: i32 = 0; i < tags.len(); i++ {
        println!(tags.at(i).as_string());   // x, then y, then z
    }
    return ok(0);
}

Error surfacing without ? — inspect .ok / .err and branch:

import stdlib::json::*;

fn main() -> i32 {
    let v: *JsonValue = JsonValue::parse("{\"age\":\"30\"}");

    let r: !i32 = v.get_int("age");        // age is a string
    if !r.ok { println!(r.err); }          // field "age" is not an int

    let m: !string = v.get_string("name"); // absent
    if !m.ok { println!(m.err); }          // missing key "name"

    let scalar: *JsonValue = JsonValue::int(5);
    let bad: !i32 = scalar.get_int("x");   // self is not an object
    if !bad.ok { println!(bad.err); }      // not an object
    return 0;
}
import stdlib::json::*;

fn main() -> i32 {
    let v: *JsonValue = JsonValue::parse("{\"a\":3,\"b\":3.0}");

    let bi: !i32 = v.get_int("b");
    if !bi.ok { println!(bi.err); }    // field "b" is not an int

    let af: !f64 = v.get_float("a");
    if af.ok { println!(af.val); }     // 3.0  (int promoted)
    let bf: !f64 = v.get_float("b");
    if bf.ok { println!(bf.val); }     // 3.0
    return 0;
}

Optional fields (-> ?T)

Method Signature none() when
opt_string fn opt_string(self, key: string) -> ?string missing key, JSON null, or not a string.
opt_int fn opt_int(self, key: string) -> ?i32 missing key, JSON null, or not a JSON int.
opt_bool fn opt_bool(self, key: string) -> ?bool missing key, JSON null, or not a bool.
opt_float fn opt_float(self, key: string) -> ?f64 missing key or not a number; a JSON int promotes to f64.

Read them with match, .has / .val, or default with ??.

import stdlib::json::*;

fn main() -> i32 {
    let v: *JsonValue = JsonValue::parse("{\"nick\":\"al\",\"n\":5}");

    match v.opt_string("nick") {
        some(s) => println!("nick=", s),
        none()  => println!("no nick"),
    }

    let port: i32 = v.opt_int("port") ?? 8080;   // 8080 (default)
    println!(port);

    let fl: ?f64 = v.opt_float("n");             // int promotes to f64
    if fl.has { println!(fl.val); }              // 5.0

    let nope: ?i32 = v.opt_int("nick");          // wrong type -> none
    if !nope.has { println!("not an int"); }
    return 0;
}

get_any

pub fn get_any(self: *JsonValue, key: string) -> *JsonValue

Reads any value at key regardless of JSON type (or null if self is not an object, or the key is absent) — useful for a dynamic pass-through field whose shape you do not want to constrain.

import stdlib::json::*;

fn main() -> i32 {
    let o: *JsonValue = JsonValue::parse("{\"meta\":[1,2],\"n\":5}");
    let raw: *JsonValue = o.get_any("meta");
    if raw != null { println!(raw.emit()); }   // [1,2]
    let miss: *JsonValue = o.get_any("nope");
    if miss == null { println!("absent"); }
    return 0;
}

The JsonBind trait

pub trait JsonBind {
    fn from_json(v: *JsonValue) -> !Self;
    fn to_json(self: *Self) -> *JsonValue;
}

Two-way JSON binding for your own structs. Implementing JsonBind lets a type flow through the typed HTTP handler helpers (json_respond, HttpResponse::ok().json_of(...), the @handler proc macro — see the HTTP chapter) without hand-rolling parse/emit ceremony each time. from_json returns !Self so element validation propagates with ?; to_json builds a *JsonValue tree.

A full round-trip, including an optional field handled with opt_int / .has:

import stdlib::json::*;

struct Pet {
    pub name: string,
    pub age:  ?i32,
}

impl JsonBind for Pet {
    fn from_json(v: *JsonValue) -> !Pet {
        if v.kind != JSON_OBJECT { return err("expected object"); }
        return ok(Pet {
            name: v.get_string("name")?,
            age:  v.opt_int("age"),
        });
    }
    fn to_json(self: *Pet) -> *JsonValue {
        let o: *JsonValue = JsonValue::object();
        o.obj_set("name", JsonValue::string(self.name));
        if self.age.has { o.obj_set("age", JsonValue::int(self.age.val)); }
        return o;
    }
}

fn main() -> !i32 {
    let p: Pet = Pet::from_json(JsonValue::parse("{\"name\":\"rex\",\"age\":4}"))?;
    println!(p.name);                      // rex
    if p.age.has { println!(p.age.val); }  // 4
    println!((&p).to_json().emit());       // {"name":"rex","age":4}

    let q: Pet = Pet::from_json(JsonValue::parse("{\"name\":\"tom\"}"))?;
    println!((&q).to_json().emit());       // {"name":"tom"}  (age omitted)
    return ok(0);
}

Blanket impl for Vector<T>

impl<T: JsonBind> JsonBind for Vector<T>

Any Vector<T> whose T implements JsonBind is itself JSON-bindable: a JSON array maps to a Vector<T> and back. Element-level from_json failures bubble up via ?, so the first bad element fails the whole parse — there is no silent skipping. Empty arrays parse to empty vectors; non-array input errors with "expected array".

import stdlib::json::*;

@derive(JsonBind)
struct Pet { pub name: string, pub age: i32 }

fn main() -> i32 {
    let xs: *Vector<Pet> = Vector::new();
    xs.push(Pet { name: "rex", age: 4 });
    xs.push(Pet { name: "tom", age: 7 });

    let js: string = xs.to_json().emit();
    println!(js);   // [{"name":"rex","age":4},{"name":"tom","age":7}]

    let good: !Vector<Pet> = Vector::from_json(JsonValue::parse(js));
    if good.ok { println!(good.val.len()); }   // 2

    // one bad element fails the whole parse
    let bad: !Vector<Pet> =
        Vector::from_json(JsonValue::parse("[{\"name\":\"a\",\"age\":1},{\"name\":\"b\",\"age\":\"x\"}]"));
    if !bad.ok { println!(bad.err); }   // field "age" is not an int

    // non-array input
    let notarr: !Vector<Pet> = Vector::from_json(JsonValue::parse("{}"));
    if !notarr.ok { println!(notarr.err); }   // expected array
    return 0;
}

The JsonParser struct

pub struct JsonParser {
    pub src: string,
    pub pos: i32,
    pub n: i32,
}

The cursor JsonValue::parse drives internally. It is public but rarely constructed directly — prefer JsonValue::parse(src), which builds a JsonParser { src: src, pos: 0, n: src.len() } for you and returns the parsed tree.

See also

  • HTTP chapter — JsonBind-powered response helpers (json_respond, HttpResponse::ok().json_of(...), the @handler proc macro) live on the HTTP side and consume the types documented here.
  • Hashmap chapter — JSON objects use insertion-ordered parallel vectors, not a hash map; reach for the hashmap module when you need keyed lookups at scale.

Caveats

  • JsonValue::parse is lenient and never returns an error; truncated or malformed input yields a best-effort tree (a stranded value becomes null, unparseable input becomes JSON_NULL). Validate kind (or use the get_* accessors, which type-check) before trusting parsed data.
  • \uXXXX escapes decode to a single ? — full UTF-16 / surrogate decoding is not yet implemented.
  • get_int rejects JSON floats (3.0) as the wrong type; use get_float when a number may legitimately carry a fractional part. get_float accepts ints.
  • JsonValue::float(n) always emits with a fractional part (3.000000), never as a bare integer. Use JsonValue::int for integral wire values.
  • obj_set appends duplicate keys rather than overwriting; get returns the first match. Build a fresh object to "replace" a value.
  • The coercing as_* accessors cannot distinguish a missing key (null) from a real zero value; reach for get_* / opt_* when that matters.