Skip to content

json

Turns a table into text, and text back into a table. A store holds text; this is how a table gets into one.

luau
local json = require("json")

encode

luau
json.encode(value) -> text | nil, reason
luau
local text = json.encode({ coins = 10, items = { "axe", "rope" } })
-- '{"coins":10,"items":["axe","rope"]}'

Called with a dot, not a colon — these two functions do not belong to a thing, they just convert.

Named fields always come out sorted, so encoding the same table twice gives the same text. That is what lets you compare two saves as plain strings and trust the answer.

decode

luau
json.decode(text) -> value | nil, reason
luau
local back = json.decode(self.store:get("save") or "{}")
print(back.coins)       -- 10, a number

Whole numbers stay whole: an id you saved comes back as the same digits, not as 1.0e15.

Both refuse instead of guessing

Neither raises. You get nil and a reason, so a script handed bad text can carry on:

luau
local value, why = json.decode(text)
if not value then
    print("could not read the save: " .. why)
    value = {}      -- start fresh
end

encode refuses when:

whatreason
the table contains itselfcannot encode a table that contains itself
a table mixes a list and named fields — { 1, 2, name = "x" }cannot encode a table that mixes list and record
a number is nan or infinityJSON has no way to write them
a string is not valid textcannot encode a string that is not valid text (UTF-8)
over 256 KB, or nested over 64 deepit says which

decode refuses malformed text and says where:

luau
json.decode('{"a":1} leftover')
-- nil, "there is leftover text after the value (at position 9)"

Leftover text is an error rather than a silent half-read — that is the shape of two saves written over each other, and reading the first half as if it were the whole thing is how a corrupted save looks fine until it doesn't.

Three things worth knowing

A list must not contain null. [1, null, 3] is refused: a Lua table with a hole is not a list, and # would lie about it.

null in a record means absent. {"a": null} decodes to a table with no a — Lua has no way to hold "present but nil".

An empty list comes back as an empty record. [] encodes again as {}, because in Lua an empty list and an empty record are the same value#t is 0 and next(t) is nil for both. It is stable: encode it again and it stays {}, so a save that round-trips twice never drifts.

Example: a save that survives anything

luau
--!strict
local self = require("self")
local json = require("json")

type Save = { owner: string, uses: number, notes: { string } }

local function load(): Save
    local raw = self.store:get("state")
    if not raw then return { owner = "", uses = 0, notes = {} } end

    local value = json.decode(raw)
    if not value then return { owner = "", uses = 0, notes = {} } end
    return value :: Save
end

local function save(s: Save)
    local text, why = json.encode(s)
    if not text then
        print("could not save: " .. why)
        return
    end
    local ok, reason = self.store:set("state", text)
    if not ok then print("could not save: " .. reason) end
end

Both refusals are checked, and neither can take the script down.

See also

  • Saving data — the recipe, including the world's own store
  • world.store — memory shared by every script in the region

Hungrit scripting documentation.