These docs are under active development and cover the v0.20 Kobicha security model.
On this page
reference 2 min read

json

json is a top-level Lua global. Two methods, both pure host-side functions (no VM round-trip).

json.encode(value)

Serialise a Lua value to a JSON string. Tables, numbers, booleans, strings, and nil round-trip naturally.

json.encode({a = 1, b = "x"})        -- '{"a":1,"b":"x"}'
json.encode({10, 20, 30})            -- '[10,20,30]'
json.encode(true)                    -- 'true'
json.encode(nil)                     -- 'null'
json.encode({a = 1, b = nil})        -- '{"a":1}'  (b isn't in the table — Lua semantics)
json.encode({[1]=1, [3]=3})          -- '[1,null,3]'  (array padded to max int key)

Array vs object detection: a table whose keys are positive integers (1..=N, possibly with gaps) encodes as a JSON array of length max(key); gaps emit null. Anything else encodes as an object. Mixed tables ({1, 2, name = "x"}) fall into the object case, with integer keys stringified.

Encoding errors (cycles, function/userdata values, non-string-coercible keys) raise a Lua error.

json.decode(string)

Parse a JSON string into a Lua value.

local t = json.decode('{"x": 42}')
print(t.x)                           -- 42

local t = json.decode('{"a": {"b": ["c", "d"]}}')
print(t.a.b[2])                      -- "d"

Number precision

JSON integers in [i64::MIN, i64::MAX] decode to Lua integers exactly — full 64-bit precision, no rounding through f64. Integers above i64::MAX (i.e. u64-only values, up to 0xFFFFFFFFFFFFFFFF) preserve their bit pattern via a wrap-cast: 0xFFFFFFFFFFFFFFFF decodes to -1, exactly matching Lua 5.4's own tonumber("0xFFFFFFFFFFFFFFFF"). Bitwise ops still work correctly on the result (the bits are the bits).

Non-integer JSON (3.14, 1e10) decodes to Lua's float type as expected.

You can drop the "emit as hex string and tonumber() it" workaround that older code used — large unsigned values now round-trip through json.decode directly.

Null handling

Lua tables can't hold nil as a value — assigning nil to a key removes it. We follow the standard Lua JSON-library convention (dkjson, lua-cjson):

Input Lua result Notes
null nil Top-level scalar.
{"k": null} A table with no k key. t.k == nil, next(t) doesn't yield k.
[1, null, 3] {1, nil, 3} — array-with-hole. t[2] == nil; #t is implementation-defined per Lua.

This is lossy versus the original "key exists but is null": round-tripping decodeencode drops null fields. Tests that need to distinguish null from absent should keep the source string and re-check.

Decoding errors raise a Lua error with the parse position.

Typical use

Reading config the guest emitted:

local body = vm:read_file("/var/lib/peios/state.json")
local state = json.decode(body)
t:assert_eq(state.ready, true)

Building a request body for a test client:

local body = json.encode({op = "write", key = "k", value = "v"})
vm:run({"curl", "-X", "POST", "-d", body, "http://localhost:8080/api"}):assert_ok()

Performance

encode and decode are pure-host serde_json calls — microseconds for typical config-sized inputs. No VM round-trip, no agent involvement. Safe to call from inside tight test loops.