std/json
Strict RFC 8259 JSON: a parser with typed accessors, a cursor API that navigates without allocating, and builders (Json.obj(), Json.arr()) for writing. Json.parseJsonc accepts comments and trailing commas.
For typed decoding into structs use @derive(Json) (see the language reference): it generates T.fromJson(text) for an object and T.fromJsonArray(text) for a document whose top level is an array of T, with toJson / T.toJsonArray(items) the other way.
from "std/json" import { Json }Example
from "std/json" import { Json }
fn main() {
let doc = Json.parse("{\"name\": \"milo\", \"tags\": [\"fast\", \"safe\"], \"meta\": {\"stars\": 9}}")!
print(doc["name"].str() ?? "unknown") // milo
print(doc["tags"][1].str() ?? "") // safe
print(doc["meta"]["stars"].i64() ?? 0) // 9
print(doc["missing"]["deeper"].exists()) // false
for tag in doc["tags"].items() {
print(doc[tag].str() ?? "")
}
// {...meta, stars: 10, lang: "milo"}
let out = Json.obj().spread(doc, doc.curPath("meta")).int("stars", 10).str("lang", "milo").build()
print(out) // {"stars":10,"lang":"milo"}
}doc["a"][0]["b"] indexes with a string key, an integer position, or a JsonCur, and ends in a read: .str(), .f64(), .i64(), .bool(), .len(), .exists(), .raw(), .json(), .items(), .keys(). A missing step is not an error; every read after it is None (or empty), so ?? default finishes the walk. It compiles to the cursor methods below with no allocation per step. A step stored in a variable is a JsonCur, which is only a position: a struct cannot hold the &Json it points into, so it is read back through the document as doc[cur]["field"]. The chain must start at a Json held in a variable or field.
A dotted path is one step: doc.at("data.user.orgs[0].login") starts a chain and doc[cur].at("stats.members") continues one (keys, and [n] for an array position, the syntax of curPath). An integer doc.at(i) is the owned element accessor instead.
The owned accessors (get, at) return a subtree as its own Json, a deep clone per call. That reads well and is fine for configuration-sized input; on a large document navigated in a loop, use indexing or the cursor methods (curRoot, curField, curChild, curStr, strAt, ...) instead.
API reference
Json
pub struct JsonA parsed document: one flat pool of nodes over the original source, plus the index side-tables the accessors walk. Treat the fields as internal and use the accessors.
Fields: source: string, nodes: Vec<JsonNode>, nums: Vec<f64>, childIdx: Vec<i64>, keyOffsets: Vec<i64>, keyLens: Vec<i64>, root: i64.
Json.arr
fn Json.arr(): JsonArrNew empty array builder.
Json.asBool
fn Json.asBool(self: &Json): Option<bool>This node as a boolean, or None if it is not one.
Json.asF64
fn Json.asF64(self: &Json): Option<f64>This node as a float, or None if it is not a number.
Json.asI64
fn Json.asI64(self: &Json): Option<i64>This node as an integer, or None if it is not one. Fractional, exponent and out-of-range literals are None, as for i64.
Json.asStr
fn Json.asStr(self: &Json): Option<string>This node as a string, or None if it is not one.
Json.at
fn Json.at(self: &Json, index: i64): Option<Json>Element index of an array as its own Json, or None if this is not an array or the index is out of range. Deep-clones like get.
Json.bool
fn Json.bool(self: &Json, key: &string): Option<bool>The boolean value of object key key, or None if absent or not a boolean.
Json.boolAt
fn Json.boolAt(self: &Json, cur: i64, p: &string): Option<bool>Boolean at path p under cursor cur, or None (see strAt).
Json.boolPath
fn Json.boolPath(self: &Json, p: &string): Option<bool>Json.curAt
fn Json.curAt(self: &Json, start: i64, p: &string): i64curPath relative to a cursor: doc.curAt(item, "cc.location"). A path that starts with an index ("[0].name") reads the cursor as an array.
Json.curBool
fn Json.curBool(self: &Json, cur: i64): Option<bool>Json.curChild
fn Json.curChild(self: &Json, cur: i64, index: i64): i64Json.curField
fn Json.curField(self: &Json, cur: i64, key: &string): i64Json.curFloat
fn Json.curFloat(self: &Json, cur: i64): Option<f64>Json.curHolds
fn Json.curHolds(self: &Json, cur: i64): boolWhether cur is a cursor into THIS document (and in range). Branch on this for a cursor you did not navigate to yourself; the accessors abort on a foreign cursor instead. False for -1. Mirrors Sealed.holds.
Json.curInt
fn Json.curInt(self: &Json, cur: i64): Option<i64>Json.curItems
fn Json.curItems(self: &Json, cur: i64): Vec<i64>Element cursors of the array at cur, so a walk reads for it in doc.curItems(a). Empty for anything that is not an array.
Json.curJson
fn Json.curJson(self: &Json, cur: i64): JsonThe node at cur as a document of its own (null for a missing cursor): what a Json field of a @derive(Json) struct decodes to.
Json.curJsonVal
fn Json.curJsonVal(self: &Json, cur: i64): JsonValThe node at cur as JsonVal.JRaw of its source text (null for a missing cursor): what a JsonVal field of a @derive(Json) struct decodes to.
Json.curKeyAt
fn Json.curKeyAt(self: &Json, cur: i64, index: i64): stringKey of the object child at index. keys() answers the same question for the ROOT only, which is no help to a nested object whose keys are not known ahead of time — decoding a HashMap<string, V> field has to walk children it cannot name. Empty string for a non-object cursor or an out-of-range index; "" is also a legal JSON key, so bound the walk with curLen rather than testing the result.
Json.curKind
fn Json.curKind(self: &Json, cur: i64): i32Node kind at the cursor: 0 null, 1 bool, 2 number, 3 string, 4 array, 5 object; -1 if the cursor is invalid.
Json.curLen
fn Json.curLen(self: &Json, cur: i64): i64Json.curPath
fn Json.curPath(self: &Json, p: &string): i64Cursor at the end of a dotted path — "a.b", "items[0].name". A missing key, an out-of-range index or a shape mismatch yields -1, which every cursor accessor already reads as "nothing here", so the walk is total for any input. A key containing '.' or '[' is not addressable this way; use get()/at().
Json.curRaw
fn Json.curRaw(self: &Json, cur: i64): stringThe JSON text of the node at cur, as rawStr gives for a whole document; "" for a missing cursor. Splicing a subtree into output this way costs one walk of that subtree, where get/at plus rawStr copied the entire document first.
Json.curRoot
fn Json.curRoot(self: &Json): i64get/at above return an owned Json by deep-cloning the whole document (source string + every node) per call — navigating a large doc thousands of times blows up to gigabytes. The cursor API instead hands out an i64 cursor into THIS document: navigation returns cursors (-1 = missing) with zero allocation, and only leaf-string reads materialize (just that one string). Start at curRoot() and chain: let user = doc.curField(doc.curChild(doc.curRoot(), 0), "name") match doc.curStr(user)
A cursor is not a bare node index: the document's brand rides in its high bits (see jsonCurResolve above). Resolving a cursor against a different Json used to read a wrong-but-in-bounds node of the other document and report nothing; now it aborts, naming the mistake. -1 still flows through every accessor as "nothing here", so navigation stays total for any in-document miss. A memberwise copy of a Json shares its brand, which is sound because copying preserves node numbering; the documents get/at/path return carry a fresh brand because extraction renumbers the pool.
This is one of two ways to reach a nested value. The other is to chain the owned accessors with Option.andThen — doc.at(0).andThen((j) => j.str("name")) — which reads better but pays one deep clone per hop. Both replaced the family of fixed-shape accessors (strAt, childI64At, …) this module used to carry; see docs/breaking-changes.md.
Json.curStr
fn Json.curStr(self: &Json, cur: i64): Option<string>Json.curUint
fn Json.curUint(self: &Json, cur: i64): Option<u64>Unsigned read, for values above i64::MAX that curInt must reject.
Json.curValueAt
fn Json.curValueAt(self: &Json, cur: i64, index: i64): i64Value of the object child at index, the other half of curKeyAt. Deliberately separate from curChild, which is the ARRAY accessor and returns -1 for an object: "element i of an array" and "entry i of an object" are different questions, and one function answering both would make an object walked as an array look like it worked.
Json.f64
fn Json.f64(self: &Json, key: &string): Option<f64>The numeric value of object key key as a float, or None if absent or not a number.
Json.f64At
fn Json.f64At(self: &Json, cur: i64, p: &string): Option<f64>Number at path p under cursor cur, or None (see strAt).
Json.f64Path
fn Json.f64Path(self: &Json, p: &string): Option<f64>Json.get
fn Json.get(self: &Json, key: &string): Option<Json>The value of object key key as its own Json, or None if this is not an object or has no such key. Deep-clones the subtree on every call; for hot navigation use the cursor API (curRoot, curField, ...).
Json.i64
fn Json.i64(self: &Json, key: &string): Option<i64>The integer value of object key key, or None if absent or not an integer. Only a literal that is an integer and fits i64 answers Some: 1.5, 1e3 and out-of-range values are None, never a truncation.
Json.i64At
fn Json.i64At(self: &Json, cur: i64, p: &string): Option<i64>Integer at path p under cursor cur, or None (see strAt).
Json.i64Path
fn Json.i64Path(self: &Json, p: &string): Option<i64>Json.idxBool
fn Json.idxBool(self: &Json, at: JsonCur): Option<bool>.bool() at the end of an index chain: the boolean at at, or None.
Json.idxCur
fn Json.idxCur(self: &Json, at: JsonCur): JsonCurdoc[cur]: re-enter an index chain at a cursor kept in a variable.
Json.idxExists
fn Json.idxExists(self: &Json, at: JsonCur): bool.exists() at the end of an index chain: whether every step found something.
Json.idxF64
fn Json.idxF64(self: &Json, at: JsonCur): Option<f64>.f64() at the end of an index chain: the number at at, or None.
Json.idxI64
fn Json.idxI64(self: &Json, at: JsonCur): Option<i64>.i64() at the end of an index chain: the integer at at, or None.
Json.idxItems
fn Json.idxItems(self: &Json, at: JsonCur): Vec<JsonCur>for it in doc[...].items(): a cursor per element of the array at at; empty for anything that is not an array.
Json.idxJson
fn Json.idxJson(self: &Json, at: JsonCur): Json.json() at the end of an index chain: the subtree at at as a document of its own (as curJson).
Json.idxKey
fn Json.idxKey(self: &Json, at: JsonCur, key: &string): JsonCurdoc[...]["key"]: the member named key of the object at at; absent when at is not an object or has no such key.
Json.idxKeys
fn Json.idxKeys(self: &Json, at: JsonCur): Vec<string>Object keys in document order, for for k in doc["m"].keys().
Json.idxKind
fn Json.idxKind(self: &Json, at: JsonCur): i32.kind() at the end of an index chain: the kind tag curKind reports; negative when absent.
Json.idxLen
fn Json.idxLen(self: &Json, at: JsonCur): i64.len() at the end of an index chain: element or member count; 0 for a scalar or an absent value.
Json.idxPath
fn Json.idxPath(self: &Json, at: JsonCur, p: &string): JsonCurdoc.at("a.b[0].c") / doc[...].at(path): the value at a dotted path under at (the syntax of curPath); absent when any step is missing.
Json.idxPos
fn Json.idxPos(self: &Json, at: JsonCur, i: i64): JsonCurdoc[...][i]: element i of the array at at; absent when out of range or not an array.
Json.idxRaw
fn Json.idxRaw(self: &Json, at: JsonCur): string.raw() at the end of an index chain: the value's JSON text (as curRaw); empty when absent.
Json.idxRoot
fn Json.idxRoot(self: &Json): JsonCurdoc["a"][0]["b"].str() is rewritten by the checker into these calls, rooted at doc every step: a JsonCur cannot read anything alone, because a struct cannot hold the &Json it points into. A cursor kept in a variable re-enters the chain as doc[cur]. A missing step yields a cursor every read treats as absent.
Json.idxStr
fn Json.idxStr(self: &Json, at: JsonCur): Option<string>.str() at the end of an index chain: the string at at, or None.
Json.idxValueAt
fn Json.idxValueAt(self: &Json, at: JsonCur, i: i64): JsonCurThe value of the ith member of the object at at, in document order (pairs with idxKeys).
Json.isArray
fn Json.isArray(self: &Json): boolJson.isBool
fn Json.isBool(self: &Json): boolJson.isNull
fn Json.isNull(self: &Json): boolJson.isNum
fn Json.isNum(self: &Json): boolJson.isObject
fn Json.isObject(self: &Json): boolJson.isStr
fn Json.isStr(self: &Json): boolJson.keys
fn Json.keys(self: &Json): Vec<string>Every key of an object, in document order.
Json.len
fn Json.len(self: &Json): i64Number of elements of an array or members of an object.
Json.obj
fn Json.obj(): JsonObjNew empty object builder: Json.obj().set("k", ...).build().
Json.parse
fn Json.parse(s: &string): Result<Json>Parse strict RFC 8259 JSON. Propagate failures with ? or match on the error. Takes a borrow because most callers hold the text in a buffer they still need; the document owns its source, so this copies once. A caller done with its owned buffer can hand it to jsonParse and skip the copy.
Json.parseJsonc
fn Json.parseJsonc(s: &string): Result<Json>Parse JSON with JSONC extensions (comments, trailing commas). Borrows like parse; jsoncParse is the owned form.
Json.path
fn Json.path(self: &Json, p: &string): Option<Json>Json.pretty
fn Json.pretty(self: &Json, indent: i64): stringThe document re-rendered with indent spaces per level (0 or less minifies). Reformats rawStr(), so what comes out is the same bytes the parse went in with, only re-spaced (see jsonPretty).
Json.rawStr
fn Json.rawStr(self: &Json): stringThe raw JSON text of this node.
Json.str
fn Json.str(self: &Json, key: &string): Option<string>The string value of object key key, or None if absent or not a string.
Json.strAt
fn Json.strAt(self: &Json, cur: i64, p: &string): Option<string>Typed reads at a path under a cursor: doc.strAt(cc, "location.route") for doc.curStr(doc.curField(doc.curField(cc, "location"), "route")).
Json.strPath
fn Json.strPath(self: &Json, p: &string): Option<string>JsonArr
pub struct JsonArrFluent array builder: jsonArr().str("a").int(2).obj(jsonObj().bool("ok", true)).build()
Fields: items: Vec<JsonVal>.
JsonArr.arr
fn JsonArr.arr(self: JsonArr, val: JsonArr): JsonArrJsonArr.bool
fn JsonArr.bool(self: JsonArr, val: bool): JsonArrJsonArr.build
fn JsonArr.build(self: &JsonArr): stringJsonArr.buildPretty
fn JsonArr.buildPretty(self: &JsonArr, indent: i64): stringbuild() with indent spaces per nesting level (0 or less minifies).
JsonArr.float
fn JsonArr.float(self: JsonArr, val: f64): JsonArrJsonArr.int
fn JsonArr.int(self: JsonArr, val: i64): JsonArrJsonArr.nil
fn JsonArr.nil(self: JsonArr): JsonArrJsonArr.obj
fn JsonArr.obj(self: JsonArr, val: JsonObj): JsonArrJsonArr.raw
fn JsonArr.raw(self: JsonArr, json: &string): JsonArrSplice a pre-serialized JSON value verbatim (caller guarantees validity).
JsonArr.str
fn JsonArr.str(self: JsonArr, val: &string): JsonArrJsonArr.val
fn JsonArr.val(self: JsonArr, val: JsonVal): JsonArrJsonCur
pub struct JsonCurA position inside a parsed Json: what doc["a"][0] evaluates to. Only doc[...] reads it (see Json.idxRoot), so it is a plain copyable number.
Fields: c: i64.
JsonError
pub enum JsonErrorVariants:
Syntax(string): The text was not JSON at all — the parser's own message. Path is empty.Missing(string): A required field was absent. Payload is the dotted path.Mismatch(JsonMismatch): A value was present at the path but of the wrong JSON kind.
JsonError.message
fn JsonError.message(self: &JsonError): stringJsonError.path
fn JsonError.path(self: &JsonError): stringDotted path to the offending value, "" for a syntax error.
JsonError.under
fn JsonError.under(self: JsonError, seg: string): JsonErrorRe-root the error one level up. Decoders build paths bottom-up so the success path never allocates a path string.
JsonError.underIndex
fn JsonError.underIndex(self: JsonError, index: i64): JsonErrorJsonEvent
pub enum JsonEventEmits one event at a time and never materializes the tree — O(depth) memory, for documents too large to hold as a flat pool (cf. Go json.Decoder.Token / Rust StreamDeserializer). jsonPull(src).next() yields tokens until End.
Variants:
StartObjectEndObjectStartArrayEndArrayKeyStrNumBoolNullEndError
JsonMember
pub struct JsonMemberFields: key: string, val: JsonVal.
JsonMismatch
pub struct JsonMismatchWhere a decode failed and what was found instead. expected and actual share one vocabulary: null, bool, number, string, array, object, "missing".
Fields: path: string, expected: string, actual: string.
JsonNode
pub struct JsonNodekind: 0=null 1=bool 2=number 3=string 4=array 5=object
24 bytes, down from 56 with one named field per kind: the pool is the parse's working set, and the wider node cost 10-15% of parse time in cache misses. The two payload words mean something different per kind; only the jsonNode* accessors and jsonNew* constructors below know the layout. null: a = 0, b = 0 bool: a = 1 for true, 0 for false string: a = strStart, b = strLen number: a = index into the document's nums table, b = (strStart << 32) | strLen array, object: a = childStart, b = (keyStart << 32) | childCount (keyStart 0 for arrays) The packed halves fit because jsonParse caps the input at 2^31-1 bytes and the pool at 2^32-1 nodes: every count fits the low 32 bits, and every source offset and key index (at most one per input byte) stays below 2^31, so the high half never reaches the sign bit. That matters because >> on i64 is an arithmetic shift: a high half of 2^31 or more would unpack negative.
Fields: kind: i32, a: i64, b: i64.
JsonObj
pub struct JsonObjFluent object builder. Methods consume and return self to enable chaining: jsonObj().str("type", "capabilities").int("seq", 3).build()
Fields:
members: Vec<JsonMember>merging: bool: Set oncespreadran: from then on a key already present is overwritten in place, which is what{...a, k: v}does in JS. Off by default so plain building stays a push, not a scan per key.
JsonObj.arr
fn JsonObj.arr(self: JsonObj, key: &string, val: JsonArr): JsonObjJsonObj.bool
fn JsonObj.bool(self: JsonObj, key: &string, val: bool): JsonObjJsonObj.boolOpt
fn JsonObj.boolOpt(self: JsonObj, key: &string, val: Option<bool>): JsonObjJsonObj.build
fn JsonObj.build(self: &JsonObj): stringJsonObj.buildPretty
fn JsonObj.buildPretty(self: &JsonObj, indent: i64): stringbuild() with indent spaces per nesting level (0 or less minifies).
JsonObj.float
fn JsonObj.float(self: JsonObj, key: &string, val: f64): JsonObjJsonObj.floatOpt
fn JsonObj.floatOpt(self: JsonObj, key: &string, val: Option<f64>): JsonObjJsonObj.int
fn JsonObj.int(self: JsonObj, key: &string, val: i64): JsonObjJsonObj.intOpt
fn JsonObj.intOpt(self: JsonObj, key: &string, val: Option<i64>): JsonObjJsonObj.nil
fn JsonObj.nil(self: JsonObj, key: &string): JsonObjJsonObj.obj
fn JsonObj.obj(self: JsonObj, key: &string, val: JsonObj): JsonObjJsonObj.put
fn JsonObj.put(self: &mut JsonObj, key: string, val: JsonVal)Append member key; after spread, overwrite an existing key in place.
JsonObj.raw
fn JsonObj.raw(self: JsonObj, key: &string, json: &string): JsonObjSplice a pre-serialized JSON value verbatim (caller guarantees validity).
JsonObj.spread
fn JsonObj.spread(self: JsonObj, doc: &Json, cur: i64): JsonObj{...src, k: v}: copy every member of the object at cur (values verbatim), after which a key added again replaces its value and keeps its position. A non-object cursor contributes nothing, as spreading null does in JS.
JsonObj.str
fn JsonObj.str(self: JsonObj, key: &string, val: &string): JsonObjJsonObj.strOpt
fn JsonObj.strOpt(self: JsonObj, key: &string, val: Option<string>): JsonObjOptional-field helpers: add the key only when the Option is Some, so a conditional field stays inside the fluent chain instead of breaking out to an if around each call.
JsonObj.val
fn JsonObj.val(self: JsonObj, key: &string, val: JsonVal): JsonObjJsonPull
pub struct JsonPullContainer stack entries: 0 = array, 1 = object expecting key, 2 = object expecting value. The colon/comma transitions flip 1<->2 so a string can be classified as Key vs Str without lookahead.
Fields: src: string, pos: i64, stack: Vec<i64>.
JsonPull.next
fn JsonPull.next(self: &mut JsonPull): JsonTokenJsonToken
pub struct JsonTokenFields:
kind: JsonEventtext: string: set for Key and Strnum: f64: set for Numflag: bool: set for Bool
JsonVal
pub enum JsonValSerialization — JsonVal value tree + fluent builders. Complements the flat-pool parser above: parse with jsonParse, build with jsonObj()/jsonArr(), serialize with .build() / jsonSer.
Variants:
JNullJBool(bool)JInt(i64)JFloat(f64)JStr(string)JArr(Vec<JsonVal>)JObj(Vec<JsonMember>)JRaw(string): Pre-serialized JSON spliced verbatim — the caller guarantees validity. Lets builders compose with fragments produced elsewhere (other builders, pass-through payloads) without a parse/rebuild round trip.
JsonVal.toJsonText
fn JsonVal.toJsonText(self: &JsonVal): stringMethod form of jsonSer, so @derive(Json) output needs no free-function import.
Functions
jsonBoolStr
pub fn jsonBoolStr(b: bool): stringtrue/false as JSON text.
jsoncParse
pub fn jsoncParse(s: string): Result<Json>jsonEscapeStr
pub fn jsonEscapeStr(s: &string): stringjsonF32Str
pub fn jsonF32Str(f: f32): stringjsonFloatStr for an f32, printed at f32 precision (0.1 stays "0.1").
jsonFloatStr
pub fn jsonFloatStr(f: f64): stringA float as JSON text. JSON has no NaN or Infinity (RFC 8259 6), so a non-finite value encodes as null, as JSON.stringify and serde_json do; printing it would emit nan/inf, which no parser (this one included) reads back.
jsonKindName
pub fn jsonKindName(kind: i32): stringHuman-readable name for a curKind result, including -1 for "no such node".
jsonParse
pub fn jsonParse(s: string): Result<Json>jsonPretty
pub fn jsonPretty(src: &string, indent: i64): stringRe-render a JSON document. indent > 0 indents nested values by that many spaces per level, exactly as JSON.stringify(v, null, indent) does; indent <= 0 minifies, dropping every byte of whitespace outside a string.
Text that does not start with '{' or '[' comes back unchanged: a bare scalar has nothing to indent, and text that is not JSON at all (a plain-text error line, an empty body) is not this function's to mangle.
jsonPull
pub fn jsonPull(src: string): JsonPulljsonQuote
pub fn jsonQuote(s: &string): stringQuote and escape a string as a JSON string literal, brackets included.
jsonSer
pub fn jsonSer(v: &JsonVal): string