Hungrit Docs

json#

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

Use it to

  • keep a whole table in a store, which only holds text
  • read it back out as a table
  • read and write the bodies of web requests
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 freshend

encode refuses when:

whatreason
the table contains itselfcannot encode a table that contains itself
a table mixes a list and named fields, such as { 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, which 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, because 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
--!strictlocal 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 :: Saveend 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) endend

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

See also#

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

Updated 17 October 2026