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
local json = require("json")encode#
json.encode(value) -> text | nil, reasonlocal 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#
json.decode(text) -> value | nil, reasonlocal back = json.decode(self.store:get("save") or "{}")print(back.coins) -- 10, a numberWhole 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:
local value, why = json.decode(text)if not value then print("could not read the save: " .. why) value = {} -- start freshendencode refuses when:
| what | reason |
|---|---|
| the table contains itself | cannot 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 infinity | JSON has no way to write them |
| a string is not valid text | cannot encode a string that is not valid text (UTF-8) |
| over 256 KB, or nested over 64 deep | it says which |
decode refuses malformed text and says where:
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#
--!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) endendBoth 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