hanki

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.