lib:json module
Stable
The lib:json module exposes structural handles for working with JSON documents in Luau. It provides two userdata types — Object and Array — and a null sentinel distinct from Lua nil. Documents are constructed with json.object or json.array, or received populated from a parser such as vnd:serde_json.
| Property | Value |
|---|---|
| Namespace | lib |
| Source | src/lua/lib/json.rs |
| Tests | tests/lib/json.test.luau |
| Stability | Stable |
| Sharable across workers | No — Object, Array, and json.null are all rejected by workers.shared |
Syntax
local json = require("lib:json")The returned table exposes module functions and the null sentinel.
Description
lib:json is explicit about access. There is no obj.field sugar, no arr[1] indexing, and no implicit #arr length. Reads go through :get(...), writes through :set(...) or :push(...), presence checks through :has(...), and length through :len(). The shape keeps document semantics legible to readers and consistent under Luau strict mode, which cannot statically check dynamic JSON shapes.
Object, Array, and the json.null sentinel are not sharable across workers. Each is bound to the Lua VM that constructed or parsed it. Scripts that need cross-worker state should reach for std:collections or another sharable type and serialise JSON content into it through vnd:serde_json.
Module functions
json.object
json.object(): ObjectReturns an empty mutable Object handle.
json.array
json.array(): ArrayReturns an empty mutable Array handle.
json.null
json.null: anyA unique userdata sentinel representing JSON null, distinct from Lua nil (which represents "key absent"). Comparable by ==. Always the same value within and across workers — serde_json.from_str("null") == json.null holds. Not sharable through std:workers.shared; passing it errors with the standard "not a sharable userdata" message.
Object methods
Object:get(...path) : Walks path through nested Objects and Arrays. Each segment is a string (object key) or integer (1-based array index). Returns a Lua scalar for terminal leaves, json.null for explicit JSON nulls, a fresh Object or Array handle for nested containers, or nil if any segment is missing. With no arguments, returns the receiver.
Object:set(key, value) : Assigns value at key. Accepts Lua scalars, plain Lua tables (recursively converted), other Object or Array handles, and json.null. Replacing a container preserves identity for handles already pointing into the old subtree at the same path — they reflect the new contents.
Object:has(key) : true if key is present (including when its value is json.null), false otherwise. Distinguishes "key absent" from "key explicitly null".
Object:keys() : Returns an array of the Object's keys. Order is not guaranteed; sort explicitly if needed.
Object:len() : Returns the number of entries in the Object.
Object:to_table() : Returns a deep Lua-table copy of the Object's current contents. Mutations to the returned table do not flow back into the document.
Array methods
Array:get(...path) : Same path-walking semantics as Object:get. The leading segment is a 1-based integer index.
Array:set(index, value) : Assigns value at the 1-based index. Accepts the same value shapes as Object:set.
Array:push(value) : Appends value to the end of the array.
Array:len() : Returns the number of elements in the array.
Errors
lib:json does not raise on missing-path reads. :get(...) returns nil when any segment fails to resolve. Type mismatches at write time follow Luau's normal error path — passing a function to :set raises synchronously.
json.null is comparable with == but is not sharable through std:workers.shared.
Examples
Parsing, navigating, and mutating
The following example parses a JSON string, reads nested fields, mutates the document, and serialises the result.
local json = require("lib:json")
local serde_json = require("vnd:serde_json")
local doc = serde_json.from_str('{"name":"Alice","age":30,"tags":["a","b"]}')
print(doc:get("name")) -- Alice
print(doc:get("tags", 2)) -- b
print(doc:has("missing")) -- false
doc:set("city", "Springfield")
doc:get("tags"):push("c")
print(serde_json.to_string(doc)) -- {"age":30,"city":"Springfield",...}Distinguishing null from absent
The following example shows how :has and :get together distinguish a key whose value is explicitly null from a key that is missing entirely.
local json = require("lib:json")
local serde_json = require("vnd:serde_json")
local v = serde_json.from_str('{"explicit":null}')
print(v:has("explicit")) -- true
print(v:get("explicit") == json.null) -- true
print(v:has("missing")) -- false
print(v:get("missing")) -- nilA nested handle observing parent mutation
A handle obtained from :get continues to reflect the live document after the parent replaces the subtree.
local serde_json = require("vnd:serde_json")
local doc = serde_json.from_str('{"nested":{"x":1}}')
local nested = doc:get("nested")
doc:set("nested", { x = 99, y = 100 })
print(nested:get("x")) -- 99
print(nested:get("y")) -- 100Building a document from scratch
The following example constructs an Array, then an Object holding the array and other values, then writes json.null into a third field.
local json = require("lib:json")
local arr = json.array()
arr:push(1)
arr:push("two")
arr:push(true)
local obj = json.object()
obj:set("name", "Alice")
obj:set("scores", arr)
obj:set("missing", json.null)Acceptance
The following scenarios must hold. They are exercised by the integration tests under tests/lib/json.test.luau.
- Constructors.
json.objectandjson.arrayreturn empty handles whose:len()is0. - Round-trip with parser.
serde_json.from_str('{"a":1}')returns an Object handle for which:get("a")is1. - Variadic path walk. For
'{"user":{"tags":["x","y"]}}',:get("user", "tags", 2)returns"y". Any missing segment yieldsnil. - Null versus absent. For
'{"explicit":null}',:has("explicit")istrueand:get("explicit") == json.null.:has("missing")isfalseand:get("missing")isnil. - Scalar JSON values.
serde_json.from_str('"hi"')returns the Lua string"hi".'42'returns42.'true'returnstrue.'null'returnsjson.null. - Nested handle observes mutation. A handle obtained via
doc:get("nested")continues to reflect the live document afterdoc:set("nested", ...)replaces the subtree. - Array push, set, len.
arr:push(x)increases:len()by one.arr:get(n)returns the element at the 1-based indexn.arr:set(n, y)overwrites that element. - Object keys.
obj:keys()returns an array containing every key present, with#obj:keys() == obj:len(). to_tableis a snapshot. Mutating the table returned by:to_table()does not affect the underlying document.- Not sharable across workers. Passing an Object, Array, or
json.nulltostd:workers.sharedraises an error containing"not a sharable userdata". Cross-worker state requires serialisation throughvnd:serde_jsoninto a sharable carrier such asstd:collections.map.
See also
vnd:serde_json— Parser and serialiser that produces and consumeslib:jsonhandles.std:workers— The cross-worker model that this module's handles do not participate in.std:collections— Sharable map and counter types for cross-worker state.vnd:jsonschema— Schema validation for JSON documents.- The module system — How
require("lib:json")resolves.