json
stdlib/extra/json.hk: JSON (RFC 8259) as a value tree.
JSON is the legacy-ecosystem interchange format (HANKI.md §16: CBOR is the primary format, JSON the compatibility shim). JSON is modelled as a plain value tree and never wired into @derive(Encode, Decode): its object model (arbitrary key order, one number type, text escapes) does not line up with the count-based, positional Serializer / Deserializer framework, and JSON therefore has its own JsonValue sum, a renderer, and a parser.
parse reads a document into a JsonValue; to_string (via Display) renders one back to compact JSON. Rendering is pure. Parsing is an action only because it assembles decoded string bytes in a BytesBuilder; it reads a cursor and a buffer and commits no OS capability (an empty effect row).
Round-trip: parse(v.to_string()) reproduces v for any JsonValue (numbers via the shortest round-tripping f64 text; object key order is preserved). Non-finite numbers, which JSON cannot express, render as null.
JsonValue
type JsonValue
Null
Bool(bool)
Num(f64)
Str(string)
Arr(List<JsonValue>)
Obj(Map<string, JsonValue>)
end
The JSON data model (RFC 8259): the six kinds of value. Num is an IEEE 754 double, matching JSON's single number type. Objects are backed by Map (hash order); rendering emits keys sorted, which makes output canonical (deterministic regardless of build order), and object key order is not otherwise significant (RFC 8259 §4: objects are unordered).
JsonError
type JsonError
Unexpected(int)
Truncated(int)
BadNumber(int)
BadEscape(int)
BadUtf8(int)
Trailing(int)
TooDeep(int)
TooManyKeys(int)
end
Why a parse failed, with the byte offset at which it was detected.
impl JsonValue
get
def get(self, key: string) -> Option<JsonValue>
The value at object key key, or None if self is not an object or has no such key.
Obj(Map.empty().insert("k", Str("v"))).get("k").and_then(|j| j.as_str()) => Some("v")
Null.get("k").and_then(|j| j.as_str()) => None
at
def at(self, i: int) -> Option<JsonValue>
The element at array index i, or None if self is not an array or i is out of range.
Arr([Str("a"), Str("b")]).at(1).and_then(|j| j.as_str()) => Some("b")
Arr([Str("a")]).at(9).and_then(|j| j.as_str()) => None
as_num
def as_num(self) -> Option<f64>
The number payload, or None on any other kind.
Num(1.5f64).as_num().map(|x| x == 1.5f64) => Some(true)
Null.as_num().map(|x| x == 1.5f64) => None
as_str
def as_str(self) -> Option<string>
The string payload, or None on any other kind.
Str("hi").as_str() => Some("hi")
Null.as_str() => None
as_bool
def as_bool(self) -> Option<bool>
The boolean payload, or None on any other kind.
Bool(true).as_bool() => Some(true)
Null.as_bool() => None
as_array
def as_array(self) -> Option<List<JsonValue>>
The array payload, or None on any other kind.
Arr([Null, Null]).as_array().map(|xs| xs.length) => Some(2)
Null.as_array().map(|xs| xs.length) => None
as_object
def as_object(self) -> Option<Map<string, JsonValue>>
The object payload, or None on any other kind.
Obj(Map.empty().insert("k", Null)).as_object().map(|m| m.length) => Some(1)
Null.as_object().map(|m| m.length) => None
null?
prop null?(self) -> bool
True only for the JSON null.
Null.null? => true
Bool(false).null? => false
impl Display<JsonValue>
to_string
def to_string(self) -> string
Renders the value as compact JSON text: no spaces, keys in the map's own order.
@no-doctest: see the round-trip tests below
_render
def _render(v: JsonValue) -> string
renderbool
def _render_bool(b: bool) -> string
rendernum
def _render_num(x: f64) -> string
JSON has no NaN or Infinity, and a non-finite number therefore renders as null (matching JavaScript's JSON.stringify).
isfinite?
def _is_finite?(x: f64) -> bool
True unless the IEEE 754 exponent (bits 52..62) is all ones (inf / nan).
quote
def quote(s: string) -> string
Render s as a JSON string literal: wrapped in " with the RFC 8259 escapes (\" \\ \b \f \n \r \t, and \u00XX for the other control bytes; non-ASCII is left raw UTF-8). Matches serde's string escaping, and reproduces a foreign string field byte for byte. Public for the field-order emitters below and for hand-built output.
quote("hi") => "\"hi\""
quote("tab\t.") => "\"tab\\t.\""
_escape
def _escape(s: string, bs: bytes) -> string
Copy non-escapable bytes in runs (slice leaves UTF-8 intact, every escapable byte being single-byte ASCII); emit a \ escape for the rest. A counting loop: per-byte recursion would consume call stack proportional to the string on the AOT tier.
escapefor
def _escape_for(b: u8) -> Option<string>
uescape
def _u_escape(b: u8) -> string
_hex
def _hex(nibble: u8) -> string
renderarr
def _render_arr(xs: List<JsonValue>) -> string
arrbody
def _arr_body(xs: List<JsonValue>) -> string
renderobj
def _render_obj(m: Map<string, JsonValue>) -> string
Object keys render in sorted order, which makes output canonical (deterministic however the object was built). Map iterates in hash order, and this matches the canonical-by-key-bytes Encode for Map.
objbody
def _obj_body(ks: List<string>, m: Map<string, JsonValue>) -> string
rendermember
def _render_member(k: string, m: Map<string, JsonValue>) -> string
JsonMember
struct JsonMember
key: string
value: string
end
One rendered member of an ordered object: a key and its already-rendered JSON value text. Build with member.
member
def member(key: string, value: string) -> JsonMember
A key/value member for object. value is JSON value text: quote a string with quote, a bool with boolean, a number with to_string, and nest with object / array.
member("port", "8080").key => "port"
member("port", "8080").value => "8080"
object
def object(members: List<JsonMember>) -> string
Render members as a JSON object in the given order (not sorted): each key quoted and joined to its value text by :, members by ,. Empty is {}.
ms: List<JsonMember> = List.empty()
object(ms) => "{}"
object(ms.append(member("ok", boolean(true)))) => "{\"ok\":true}"
membertext
def _member_text(m: JsonMember) -> string
array
def array(items: List<string>) -> string
Render a JSON array from already-rendered element texts, in order. Empty is [].
xs: List<string> = List.empty()
array(xs) => "[]"
array(xs.append(boolean(true)).append("null")) => "[true,null]"
boolean
def boolean(b: bool) -> string
Render a JSON boolean as text: the scalar companion to quote for an object / array value.
boolean(true) => "true"
boolean(false) => "false"
parse
def parse(input: string) -> Result<JsonValue, JsonError>
Parse one JSON document from text. The whole input must be a single value, optionally surrounded by whitespace; trailing non-whitespace is a Trailing error.
parse("{\"k\": 1}").map(|v| v.get("k").and_then(|j| j.as_num()).map(|x| x == 1.0f64)).unwrap_or(None) => Some(true)
at = match parse("[1] junk")
Err(Trailing(i)) -> i
_ -> -1
end
at => 4
parse_bytes
def parse_bytes(input: bytes) -> Result<JsonValue, JsonError>
Parse one JSON document from raw UTF-8 bytes: the same as parse without requiring the caller to validate UTF-8 first (a string's bytes inside a value that aren't valid UTF-8 surface as BadUtf8).
parse_bytes("true".to_bytes()).map(|v| v.as_bool()).unwrap_or(None) => Some(true)
BytesReader
BytesReader, or bytes.BytesReader, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.
_finish!
def _finish!(r: BytesReader, v: JsonValue) -> Result<JsonValue, JsonError>
skipws!
def _skip_ws!(r: BytesReader) -> ()
maxdepth
def _max_depth() -> int
The deepest array/object nesting parse will descend into before returning TooDeep. Bounds the recursive-descent call stack so untrusted input cannot overflow it; well past any realistic document.
maxobject_keys
def _max_object_keys() -> int
Largest number of members decoded into a single JSON object. Objects are backed by a Map whose hash is unkeyed FNV-1a (deterministic across the bytecode and AOT tiers, hence attacker-predictable), and untrusted keys that collide on the full 64-bit hash would pile into one HAMT collision bucket and turn object construction quadratic, a hash-flooding DoS. Bounding the per-object key count caps that worst case; the ceiling is well past any realistic document (mirrors _max_depth's generous-but-bounded stance, and the http header-count bound _max_headers).
parsevalue!
def _parse_value!(r: BytesReader, depth: int) -> Result<JsonValue, JsonError>
depth is the nesting level of the value about to be read (the top-level value is 0); each array/object descends its elements one level deeper.
isdigit?
def _is_digit?(c: u8) -> bool
parselit!
def _parse_lit!(r: BytesReader, lit: string, val: JsonValue) -> Result<JsonValue, JsonError>
parsestring_value!
def _parse_string_value!(r: BytesReader) -> Result<JsonValue, JsonError>
parsestring!
def _parse_string!(r: BytesReader) -> Result<string, JsonError>
Read a quoted string starting at the opening ".
BytesBuilder
BytesBuilder, or bytes.BytesBuilder, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.
strbody!
def _str_body!(r: BytesReader, out: BytesBuilder, start: int) -> Result<string, JsonError>
A counting loop over the string's bytes: per-char recursion would consume call stack proportional to the value's length on the AOT tier.
finishstring!
def _finish_string!(out: BytesBuilder, start: int) -> Result<string, JsonError>
escapebyte!
def _escape_byte!(r: BytesReader, out: BytesBuilder) -> Result<(), JsonError>
Decode the escape after a consumed \, appending its bytes to out.
simpleescape
def _simple_escape(c: u8) -> Option<u8>
_unicode!
def _unicode!(r: BytesReader, out: BytesBuilder, esc: int) -> Result<(), JsonError>
A \uXXXX escape (the u already consumed): combine a surrogate pair into one code point, then encode it as UTF-8 into out.
unicodecp!
def _unicode_cp!(r: BytesReader, out: BytesBuilder, esc: int, hi: i32) -> Result<(), JsonError>
_surrogate!
def _surrogate!(r: BytesReader, out: BytesBuilder, esc: int, hi: i32) -> Result<(), JsonError>
_combine!
def _combine!(out: BytesBuilder, esc: int, hi: i32, lo: i32) -> Result<(), JsonError>
_hex4!
def _hex4!(r: BytesReader, esc: int) -> Result<i32, JsonError>
hex4val
def _hex4_val(b: bytes, esc: int) -> Result<i32, JsonError>
_nibble
def _nibble(b: u8) -> Option<i32>
pushutf8!
def _push_utf8!(out: BytesBuilder, cp: i32) -> ()
Encode a code point as UTF-8 into out.
_u8
def _u8(n: i32) -> u8
parsenumber!
def _parse_number!(r: BytesReader) -> Result<JsonValue, JsonError>
tonum
def _to_num(lex: string, p: int) -> Result<JsonValue, JsonError>
scannumber!
def _scan_number!(r: BytesReader, out: BytesBuilder) -> ()
isnumber_byte?
def _is_number_byte?(c: u8) -> bool
parsearray!
def _parse_array!(r: BytesReader, depth: int) -> Result<JsonValue, JsonError>
arrayelems!
def _array_elems!(r: BytesReader, acc: List<JsonValue>, depth: int) -> Result<JsonValue, JsonError>
afterelem!
def _after_elem!(r: BytesReader, acc: List<JsonValue>, depth: int) -> Result<JsonValue, JsonError>
parseobject!
def _parse_object!(r: BytesReader, depth: int) -> Result<JsonValue, JsonError>
objectmembers!
def _object_members!(r: BytesReader, acc: Map<string, JsonValue>, depth: int, budget: int) -> Result<JsonValue, JsonError>
afterkey!
def _after_key!(r: BytesReader, acc: Map<string, JsonValue>, k: string, depth: int, budget: int) -> Result<JsonValue, JsonError>
aftermember!
def _after_member!(r: BytesReader, acc: Map<string, JsonValue>, depth: int, budget: int) -> Result<JsonValue, JsonError>
JsonShapeError
type JsonShapeError
WrongKind(string)
MissingKey(string)
AtKey(string, JsonShapeError)
AtIndex(int, JsonShapeError)
end
Why a JsonValue didn't fit the target type, with the path to the failure.
impl Display<JsonShapeError>
to_string
def to_string(self) -> string
Renders the failure as a path into the offending document, and a nested cause therefore prints as .user.tags[2]: expected string.
@no-doctest: structured decode error; covered by the FromJson tests
FromJson
trait FromJson
from_json
def from_json(v: JsonValue) -> Result<Self, JsonShapeError>
Rebuild a value of Self from v, or report how it didn't fit.
string.from_json(Str("hi")).unwrap_or("?") => "hi"
impl FromJson<f64>
from_json
def from_json(v: JsonValue) -> Result<f64, JsonShapeError>
Decodes a JSON number; any other kind is a WrongKind failure.
f64.from_json(Num(1.5f64)).map(|x| x == 1.5f64).unwrap_or(false) => true
kind = match f64.from_json(Null)
Err(WrongKind(k)) -> k
_ -> "?"
end
kind => "number"
impl FromJson<string>
from_json
def from_json(v: JsonValue) -> Result<string, JsonShapeError>
Decodes a JSON string; any other kind is a WrongKind failure.
string.from_json(Str("hi")).unwrap_or("?") => "hi"
kind = match string.from_json(Null)
Err(WrongKind(k)) -> k
_ -> "?"
end
kind => "string"
impl FromJson<bool>
from_json
def from_json(v: JsonValue) -> Result<bool, JsonShapeError>
Decodes a JSON boolean; any other kind is a WrongKind failure.
bool.from_json(Bool(true)).unwrap_or(false) => true
kind = match bool.from_json(Null)
Err(WrongKind(k)) -> k
_ -> "?"
end
kind => "boolean"
listfrom_json
def _list_from_json<T: FromJson>(xs: List<JsonValue>) -> Result<List<T>, JsonShapeError>
impl<T: FromJson> FromJson<List<T>>
from_json
def from_json(v: JsonValue) -> Result<List<T>, JsonShapeError>
Decodes a JSON array element-wise; a failing element reports its index.
Pin the element type at the call site; the decode is type-directed:
got: Result<List<f64>, JsonShapeError> = List.from_json(Arr([Num(1.0f64), Num(2.0f64)]))
got.map(|xs| xs.length).unwrap_or(0) => 2
impl<T: FromJson> FromJson<Option<T>>
from_json
def from_json(v: JsonValue) -> Result<Option<T>, JsonShapeError>
JSON null is None; anything else is Some of the decoded payload.
none: Result<Option<f64>, JsonShapeError> = Option.from_json(Null)
none.map(|o| o.map(|x| x == 1.5f64)).unwrap_or(Some(false)) => None
some: Result<Option<f64>, JsonShapeError> = Option.from_json(Num(1.5f64))
some.map(|o| o.map(|x| x == 1.5f64)).unwrap_or(None) => Some(true)
mapfrom_json
def _map_from_json<V: FromJson>(ks: List<string>, m: Map<string, JsonValue>) -> Result<Map<string, V>, JsonShapeError>
impl<V: FromJson> FromJson<Map<string, V>>
from_json
def from_json(v: JsonValue) -> Result<Map<string, V>, JsonShapeError>
Decodes a JSON object value-wise, keeping the keys; a failing value reports its key.
got: Result<Map<string, f64>, JsonShapeError> = Map.from_json(Obj(Map.empty().insert("k", Num(1.0f64))))
got.map(|m| m.length).unwrap_or(0) => 1
field
def field<T: FromJson>(obj: JsonValue, key: string) -> Result<T, JsonShapeError>
Decode a required object field of obj to a FromJson type, locating any error under key. Takes the JsonValue object directly (no open map or destructuring at the use site); pin the field type at the call site, e.g. port: Result<f64, JsonShapeError> = field(obj, "port"). WrongKind if obj isn't an object, MissingKey if the key is absent.
obj = Obj(Map.empty().insert("port", Num(8080.0f64)))
port: Result<f64, JsonShapeError> = field(obj, "port")
port.map(|x| x == 8080.0f64).unwrap_or(false) => true
missing: Result<f64, JsonShapeError> = field(obj, "host")
absent = match missing
Err(MissingKey(k)) -> k
_ -> "?"
end
absent => "host"
ToJson
trait ToJson
to_json
def to_json(self) -> JsonValue
Convert self to its JsonValue representation.
"hi".to_json().as_str() => Some("hi")
impl ToJson<f64>
to_json
def to_json(self) -> JsonValue
Encodes the number as a JSON number.
1.5f64.to_json().as_num().map(|x| x == 1.5f64) => Some(true)
impl ToJson<string>
to_json
def to_json(self) -> JsonValue
Encodes the text as a JSON string.
"hi".to_json().as_str() => Some("hi")
impl ToJson<bool>
to_json
def to_json(self) -> JsonValue
Encodes the boolean as a JSON boolean.
true.to_json().as_bool() => Some(true)
impl<T: ToJson> ToJson<List<T>>
to_json
def to_json(self) -> JsonValue
Encodes the list as a JSON array, element-wise.
[1.0f64, 2.0f64].to_json().as_array().map(|xs| xs.length) => Some(2)
impl<T: ToJson> ToJson<Option<T>>
to_json
def to_json(self) -> JsonValue
None is JSON null; Some(x) is x's own encoding.
absent: Option<f64> = None
absent.to_json().null? => true
Some(1.5f64).to_json().as_num().map(|x| x == 1.5f64) => Some(true)
mapto_json
def _map_to_json<V: ToJson>(ks: List<string>, m: Map<string, V>) -> Map<string, JsonValue>
impl<V: ToJson> ToJson<Map<string, V>>
to_json
def to_json(self) -> JsonValue
Encodes the map as a JSON object, value-wise.
Map.empty().insert("k", 1.0f64).to_json().get("k").and_then(|j| j.as_num()).map(|x| x == 1.0f64) => Some(true)
nestarray
def _nest_array(n: i32, acc: string) -> string
Wrap acc in n levels of [...], building an n-deep array document.
FOLDPROBE
_FOLD_PROBE: Result<JsonValue, JsonError> = parse("{\"k\": [1, true, null]}")
Top-level bindings are compile-time-evaluated, which makes this binding the probe: the module does not compile at all unless parse folds (HANKI.md section 16). That is the comptime-JSON-constant capability, pinned here to fail a fold-set regression in this build and never in a downstream meta const.