xiao.json New

Turn Lua values into JSON text and back, with explicit rules for arrays, objects, null and numbers.

xiao.json converts Lua values to JSON text and JSON text to Lua values.

It is pure computation: it touches no file and no game state, so it works everywhere, including inside a packet hook or a cmd hook (mind their 2 ms budget on big tables, see Execution contexts). Every failure raises an error that says what and where: wrap calls in pcall when the input comes from outside.

To keep a table on disk across runs, xiao.workspace does the file handling and uses this encoder, with the same conversion rules.

At a glance

Function or value Description
xiao.json.encode(value, opts?) Lua value to JSON text
xiao.json.decode(text) JSON text to Lua value
xiao.json.array(t?) Mark a table as a JSON array, even when empty
xiao.json.null The JSON null, usable inside tables
Limit Value
Nesting depth 128 levels, for encode and decode
Values per encode 1048576
Text size 8 MiB, input of decode and output of encode

Conversion rules

Lua JSON Back to Lua
nil, xiao.json.null null xiao.json.null
true, false true, false same
whole number, -2^53 to 2^53 integer, exact (5, -3) number
other finite number shortest round trip float (0.5, 1e+300) number
string, valid UTF-8 string, UTF-8 kept as is, " \ and control characters escaped string
table with keys exactly 1..n, n >= 1 array table marked with xiao.json.array
table marked with xiao.json.array array, [] when empty table marked with xiao.json.array
other table, empty included object, keys sorted by byte value plain table
  • An empty table is an object {} by default. Use xiao.json.array() for [].
  • Object keys must be strings. A number key outside a 1..n sequence ({[5] = true}, a table with a hole, {[1.5] = 1}) raises an error: convert it with tostring first.
  • Integers in a JSON text beyond 2^53 are rounded to the nearest double, like every Lua 5.1 number.
  • A duplicate key in a JSON object keeps the last value.
  • nil cannot live in a Lua table: use xiao.json.null to keep a null in an array or an object.
  • Metatables are ignored, except the xiao.json.array mark. Vectors and colors become objects ({"x":1,"y":2}).

Functions

xiao.json.encode(value, opts): string

xiao.json.encode(value: any, opts?: {pretty?: boolean, indent?: integer}): string

Returns the JSON text of value. The output is compact (no spaces) unless opts asks for pretty printing.

Name Type Description
value any the value to encode, nil gives "null"
opts.pretty boolean one value per line, indented by 2 spaces. false forces compact output
opts.indent integer 1 to 8: width of one indentation level, turns pretty printing on

Returns: the JSON text. Errors: every message starts with json encode: and ends with the path of the faulty value, such as json encode: cannot encode a function at $.hooks[2]:

  • cannot encode a function, a thread or a userdata; cannot encode NaN, cannot encode infinity
  • string is not valid UTF-8, object key is not valid UTF-8
  • object key must be a string, got number 5
  • xiao.json.array table has keys other than 1 to n
  • cycle detected, nested deeper than 128 levels, more than 1048576 values
  • encoded text is 9000000 bytes, the limit is 8388608, without a path

A bad opts raises bad argument #2 to 'encode' (...) with unknown option 'name', option names must be strings, pretty must be a boolean or indent must be an integer from 1 to 8.

local json = xiao.json
print(json.encode({name = "steve", hp = 20, tags = {"pvp", "afk"}}))
print(json.encode({a = {1, 2}}, {pretty = true}))
{"hp":20,"name":"steve","tags":["pvp","afk"]}
{
  "a": [
    1,
    2
  ]
}

xiao.json.decode(text): any

xiao.json.decode(text: string): any

Parses a complete JSON text and returns its value. Standard JSON only: no comments, no trailing comma, nothing after the value. A UTF-8 byte order mark at the start is skipped.

Name Type Description
text string the JSON text, at most 8 MiB

Returns: the value, see Conversion rules. null gives xiao.json.null, arrays are marked with xiao.json.array. Errors: every message starts with json decode: .

  • a syntax error gives the line and the column: json decode: parse error at line 1, column 8: syntax error while parsing object key - unexpected '}'; expected string literal
  • number overflow parsing '1e400' for a number out of the double range
  • nested deeper than 128 levels
  • text is 9000000 bytes, the limit is 8388608
local ok, value = pcall(xiao.json.decode, '{"x": 1, "list": [1, null, 3]}')
if ok then
    print(value.x, #value.list, value.list[2] == xiao.json.null)
else
    print("bad json:", value)
end

xiao.json.array(t): table

xiao.json.array(t?: table): table

Marks t (or a new empty table) as a JSON array and returns it. A marked table is always written as an array, [] when empty, and decode marks every array it creates, so a decoded [] encodes back as [].

Name Type Description
t table optional table to mark, it must not already have another metatable

Returns: t, or a new empty marked table. Errors: bad argument #1 to 'array' (table already has a metatable). Marking a table twice is fine. Encoding a marked table with keys other than 1..n raises xiao.json.array table has keys other than 1 to n.

print(xiao.json.encode({items = xiao.json.array()}))
{"items":[]}

xiao.json.null

xiao.json.null: userdata

The JSON null. Compare with ==: value == xiao.json.null. tostring(xiao.json.null) is "xiao.json.null". Encoding nil also gives null, but only xiao.json.null can be stored in a table.


Complete example

Keep a list of seen players in a file and reload it on the next run (see xiao.fs, or xiao.workspace which does the same with less code).

local json, fs = xiao.json, xiao.fs
local path = "data/seen.json"

local seen = {}
if fs.isfile(path) then
    local ok, value = pcall(json.decode, fs.read(path))
    if ok and type(value) == "table" then seen = value end
end

xiao.signal.add("entity_add", "seen", function(entity)
    if entity.is_player then
        seen[entity.name] = {last = os.date("%Y-%m-%d %H:%M"), x = entity.x, z = entity.z}
    end
end)

xiao.signal.add("unload", "seen", function()
    fs.makefolder("data")
    fs.write(path, json.encode(seen, {pretty = true}))
end)