The library lljson

Quick start

If you want to store tables in linkset data or send tables to another script with linked messages, reading this section is enough:

We can encode any kind of table with its nested tables, no matter how complex the structure is, in a string and decode the string to the same table with:

  • to encode: myString = lljson.slencode(myTab)
  • to decode: myTab = lljson.sldecode(myString)

Example storing a table in linkset data:

-- storing a table in linkset data
local resort = {
    name = "Sunny Sands",
    activities = {"Sunbathing", "Jet skiing", "Frisbee competitions"},
    rating = 4.2,
    open = true,
    tips = {"Bring sunscreen", "Smile for photos", "Don't feed the seagulls"}
}

-- writing
ll.LinksetDataWrite("resort", lljson.slencode(resort))

-- reading
resort = lljson.sldecode(ll.LinksetDataRead("resort"))

-- checking that it has worked
for k, v in resort do
    print(k, if type(v) == "table" then table.concat(v, ", ") else v)
end
-- > name        Sunny Sands
-- > activities  Sunbathing, Jet skiing, Frisbee competitions
-- > rating      4.2
-- > open        true
-- > tips        Bring sunscreen, Smile for photos, Don't feed the seagulls

Example sending a table in a linked message:

-- script1 - sending a table with a linked message
local resort = {
    name = "Sunny Sands",
    activities = {"Sunbathing", "Jet skiing", "Frisbee competitions"},
    rating = 4.2,
    open = true,
    tips = {"Bring sunscreen", "Smile for photos", "Don't feed the seagulls"}
}

LLEvents:on("touch_start", function(events)
    -- sending
    ll.MessageLinked(LINK_THIS, 0, lljson.slencode(resort), "")
end)
-- script2 - receiving a table with a linked message
local resort = {}

LLEvents:on("link_message", function(sender_num, num, str, id)
    -- receiving
    resort = lljson.sldecode(str)
	
    -- checking that it has worked
    for k, v in resort do
        print(k, if type(v) == "table" then table.concat(v, ", ") else v)
    end
	-- > name        Sunny Sands
	-- > activities  Sunbathing, Jet skiing, Frisbee competitions
	-- > rating      4.2
	-- > open        true
	-- > tips        Bring sunscreen, Smile for photos, Don't feed the seagulls
end)

There is much more about the lljson library, go on reading…

What is JSON?

JSON, short for JavaScript Object Notation, is a data format used to store and exchange information.
It is simple and readable, using a clean, minimal structure that is easy to understand. It’s widely supported. It’s supported by nearly all programming languages and tools, making it ideal for exchanging data between systems.

SLua tables fit well in the JSON format, making the conversion much easier than in LSL.

JSON is useful, in general, to exchange complex multilevel structured data between:

  • external resources, using http.
  • scripts, using linked messages.
  • objects, using say and listen.
  • Linkset Data storage.

JSON organizes data into two basic structures:

  • Arrays: list of values, like the array tables in SLua:
    • [ “apple”, “banana”, “cherry” ]
    • The values are separated by commas (,) and the entire array is enclosed in brackets ([ and ]).
  • Objects: collections of (key, value) pairs, like the dictionary tables in SLua:
    • { “type” : “dog” , “name” : “Dufa”, “color” : “white” }
    • Keys and values are separated by colons (:), pairs are separated by commas (,), and the entire object is enclosed in curly braces ({ and }).
    • Keys must be strings.

There are 4 data types:

  • String: text enclosed in double quotes (“hello”).
  • Number: Numbers, either integers (42) or decimals (3.14).
  • Boolean: true or false.
  • Null: null, a special value indicating no data.

Objects and arrays can contain a mix of these types.
They also can contain other objects and arrays, in any depth and complexity.
We can have arrays of objects with keys that are arrays with more arrays and with more objects, etc. Up to 100 levels of depth.

Library lljson

The functions to work with JSON have their own library: lljson.

There are two pairs of functions:

  • lljson.encode() / lljson.decode() :
    • They generate or read standard JSON
    • They are useful to exchange data with external resources and other scripting languages.
    • SLua types without a JSON equivalent are encoded as strings.
  • lljson.slencode() / lljson.sldecode() :
    • They generate non-standard JSON-like code.
    • They are useful to exchange code with other scripts or objects or to store in linkset data.
    • They are decoded into the original SLua types.

encode()

It takes an SLua table and generates standard JSON to send to an external website or for use with another scripting language.

-- array table, encodes to a JSON array
local fruits = { "apples", "bananas", "oranges" }
print(lljson.encode(fruits))
-- > ["apples","bananas","oranges"]
-- dictionary table, encodes to a JSON object
local fruitsQuantity = { Apple = 50, Banana = 30, Cherry = 20, Orange = 15 }
print(lljson.encode(fruitsQuantity))
-- > {"Apple":50,"Cherry":20,"Orange":15,"Banana":30}

To work in SLua scripts with JSON strings that can be encoded and decoded as the same table use slencode()/sldecode().

Datatypes mapping with lljson.encode():

SLua type JSON type format
nil null
lljson.null null
boolean boolean
number number
string string
array table array []
dictionary table object {} keys encoded as strings
vector string "<25,50,10>"
rotation/quaternion string "<0.5,0.25,0,1>"
uuid string
buffer string encoded as base64 string
function run-time error
thread (coroutine) run-time error

lljson.null

lljson.null is a constant in the lljson library.
We can use it in dictionary tables when we want to export a key that has no value.

-- dictionary table (with null keys), encodes to a JSON object
local fruitsQuantity = { Apple = 50, Banana = 30, Cherry = 20, Orange = 15, Kiwi = lljson.null }
print(lljson.encode(fruitsQuantity))
-- > {"Kiwi":null,"Apple":50,"Cherry":20,"Orange":15,"Banana":30}

Empty tables

Empty tables are exported as objects:

-- empty table as JSON object
local tab = {}
print(lljson.encode(tab))
--> {}

We can use the constant lljson.empty_array to generate an empty JSON array:

-- empty table as JSON array
local tab = lljson.empty_array
print(lljson.encode(tab))
--> []

We can export an empty table as JSON array setting the table lljson.empty_array_mt as its metatable:

-- empty table as JSON array
local tab = { "hello" }
setmetatable(tab, lljson.empty_array_mt)
print(lljson.encode(tab))
--> ["hello"]
table.remove(tab, 1)
print(lljson.encode(tab))
--> []

issue : with mixed tables only the array part of the table is exported. This will be fixed to export all the table.

Sparse arrays

Moderately sparse arrays are exported as JSON objects.
nil values are exported as null.

-- sparse array as JSON array
local vegetables = { "Carrot", "Tomato", "Potato", "Onion", "Lettuce" }
vegetables[4] = nil
print(lljson.encode(vegetables))
-- > ["Carrot","Tomato","Potato",null,"Lettuce"]

If there are more nils than elements and the last index is bigger than 10, it throws the run-time error “Cannot serialise table: excessively sparse array”:

-- sparse array as JSON array, up to index 10 it works no matter how many nils
local tab = {}
tab[10] = "value 10"
print(lljson.encode(tab))
-- > [null,null,null,null,null,null,null,null,null,"value 10"]
tab[11] = "value 11"
print(lljson.encode(tab))
-- > Cannot serialise table: excessively sparse array
-- sparse array as JSON array, it works if there aren't more nils than values
local tab = {}
for i = 1, 15, 2 do
    tab[i] = "value " .. i
end
print(lljson.encode(tab))
-- > ["value 1",null,"value 3",null,"value 5",null,"value 7",null,"value 9",null,"value 11",null,"value 13",null,"value 15"]
tab[20] = "value 20"
print(lljson.encode(tab))
-- > Cannot serialise table: excessively sparse array

possible improvement : it is requested that excessively sparse arrays are exported as objects, or to have an option to export array tables as objects.

Mixed tables

Mixed tables are exported as JSON objects.
Numeric keys are exported as strings:

-- mixed table to JSON object with string keys
local vegetables = { "Carrot", "Tomato", "Potato", Lettuce = "favorite" }
print(lljson.encode(vegetables))
-- > {"1":"Carrot","2":"Tomato","3":"Potato","Lettuce":"favorite"}

We can export only the array part of a mixed table as JSON array setting the table lljson.array_mt as its metatable:

-- array part of mixed table to JSON array
local vegetables = { "Carrot", "Tomato", "Potato", Lettuce = "favorite" }
setmetatable(vegetables, lljson.array_mt)
print(lljson.encode(vegetables))
-- > ["Carrot","Tomato","Potato"]

Dictionary tables

Keys with data types other than numbers and strings throw the run-time error “Cannot serialise userdata: table key must be a number or string”:

-- dictionary table with other data types
local staff = { [ll.GetOwner()] = "VIP" }
print(lljson.encode(staff))
-- > Cannot serialise userdata: table key must be a number or string

possible improvement : it is requested that all datatypes (except functions and threads) are exported as strings.

inf and -inf

inf and -inf are exported as numbers with values 1e9999 and -1e9999:

-- inf and -inf to numbers
local bigNumbers = { math.huge, -math.huge }
print(lljson.encode(bigNumbers))
-- > [1e9999,-1e9999]

nan

nan is exported as the literal NaN (a literal, not a string).

-- nan to the literal NaN
local puffedNumbers = { 0/0 }
print(lljson.encode(puffedNumbers))
-- > [NaN]

issue : this is not standard JSON. This will be changed to export as null.

Special characters

Characters with ASCII codes from 0 to 31 are encoded as JSON unicode:

-- special characters to JSON unicode
local idBytes = ll.GetOwner().bytes
print(idBytes)
-- > ??8NK_?Έm?;?A
print(lljson.encode(idBytes))
-- > "\u000f\u0016??8NK_?Έm?;?A"

The cursor control characters have their own escape codes:

-- cursor control characters to JSON escape codes
local s = string.char(8, 9 ,10, 12, 13)
print(lljson.slencode(s))
-- > "\b\t\n\f\r"

Indexing (0 vs 1)

JSON arrays do not store explicit index values in the file. They only define an ordered list of elements.

  • When a programming language decodes a JSON array received from SLua, it assigns indices according to its own array indexing rules. Most languages use 0-based indexing, so decoded arrays start at index 0.
  • When SLua decodes a JSON array received from an external source, it creates SLua tables using 1-based indexing, so the first element starts at index 1.

Examples

A longer example:

local shelter = {
    -- General info about the shelter
    info = {
        name = "Happy Tails Shelter",
        location = "Riverdale",
        open_hours = { "Mon-Fri", "10:00-18:00" },
        staff = {
            manager = "Alice",
            volunteers = { "Bob", "Clara", "Dylan" }
        }
    },
    -- Pets section: dictionary of species
    pets = {
        dogs = {
            count = 8,
            breeds = { "Labrador", "Beagle", "Poodle" },
            vaccinated = true
        },
        cats = {
            count = 6,
            breeds = { "Siamese", "Maine Coon" },
            vaccinated = true
        },
        ["tropical fish"] = {
            count = 15,
            species = { "Guppy", "Goldfish", "Betta" },
            vaccinated = false
        }
    },
    -- Adoption records (array of dictionaries)
    adoptions = {
        { pet = "dog", adopter = "Emma", date = "2025-10-01" },
        { pet = "cat", adopter = "Lucas", date = "2025-10-12" }
    }
}

print(lljson.encode(shelter))

There are several websites where you can copy-paste JSON and show it well formatted, like https://jsonlint.com/.

Generated JSON:

{
  "pets": {
    "dogs": {
      "breeds": [
        "Labrador",
        "Beagle",
        "Poodle"
      ],
      "count": 8,
      "vaccinated": true
    },
    "tropical fish": {
      "vaccinated": false,
      "species": [
        "Guppy",
        "Goldfish",
        "Betta"
      ],
      "count": 15
    },
    "cats": {
      "breeds": [
        "Siamese",
        "Maine Coon"
      ],
      "count": 6,
      "vaccinated": true
    }
  },
  "info": {
    "location": "Riverdale",
    "open_hours": [
      "Mon-Fri",
      "10:00-18:00"
    ],
    "name": "Happy Tails Shelter",
    "staff": {
      "volunteers": [
        "Bob",
        "Clara",
        "Dylan"
      ],
      "manager": "Alice"
    }
  },
  "adoptions": [
    {
      "date": "2025-10-01",
      "pet": "dog",
      "adopter": "Emma"
    },
    {
      "date": "2025-10-12",
      "pet": "cat",
      "adopter": "Lucas"
    }
  ]
}

An example with SLua data types:

local build = {
    build_id = uuid("a1b2c3d4-1111-2222-3333-abcdefabcdef"),
    info = {
        name = "Beach Hangout",
        owner = uuid("9f8e7d6c-5555-4444-3333-bbbbbbbbbbbb"),
        category = "Social"
    },
    props = {
        {
            name = "Palm Tree",
            position = vector(128, 64, 22.5),
            orientation = rotation(0, 0, 0, 1)
        },
        {
            name = "Campfire",
            position = vector(130.5, 63.2, 21.8),
            orientation = rotation(0, 0.3827, 0, 0.9239),
            effects = {
                sound = "fire-crackle",
                color = vector(1, 0.5, 0.2)
            }
        }
    }
}

print(lljson.encode(build))

Generated JSON:

{
  "props": [
    {
      "orientation": "<0,0,0,1>",
      "name": "Palm Tree",
      "position": "<128,64,22.5>"
    },
    {
      "orientation": "<0,0.3827,0,0.9239>",
      "effects": {
        "sound": "fire-crackle",
        "color": "<1,0.5,0.2>"
      },
      "name": "Campfire",
      "position": "<130.5,63.2,21.8>"
    }
  ],
  "info": {
    "owner": "9f8e7d6c-5555-4444-3333-bbbbbbbbbbbb",
    "name": "Beach Hangout",
    "category": "Social"
  },
  "build_id": "a1b2c3d4-1111-2222-3333-abcdefabcdef"
}

decode()

It takes standard JSON received from an external website and generates an SLua table.

An example sending a request to a website that returns JSON data containing a random quote:

local url  = "https://zenquotes.io/api/random"

LLEvents:on("touch_start", function(events)
    ll.HTTPRequest(url, {}, "")
end)

LLEvents:on("http_response", function(request_id, status, metadata, body)
    if status == 200 then
        print("json: ", body)
        local quote = lljson.decode(body)
        print("quote:", quote[1].q)
        print("author:", quote[1].a)
    else
        print("Request failed.")
    end
end)

The received JSON:

[
  {
    "q": "Remember that sometimes not getting what you want is a wonderful stroke of luck.",
    "a": "Dalai Lama",
    "h": "
“Remember that sometimes not getting what you want is a wonderful stroke of luck.” —
Dalai Lama
" } ]

To work in SLua scripts with JSON strings that can be encoded and decoded as the same table use slencode()/sldecode().

Datatypes mapping with lljson.decode():

JSON type SLua type
null lljson.null
boolean boolean
number number
string string
array [] table
object {} table

lljson.null

JSON nulls are decoded to lljson.null to preserve the keys with null values from JSON objects. These keys would disappear decoding nulls to nil.

Example

An example with encode() and decode(), sending a JSON to a website that returns the same JSON and information about the headers of the request:

local url = "https://postman-echo.com/post"

local function send()
    local sending = { task = "test", value = 123, object_name = ll.GetObjectName()}
    local json = lljson.encode(sending)
    ll.HTTPRequest(url, {HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json"}, json)
end

local function receive(json)
    local receiving = lljson.decode(json)
    for k, v in receiving do
        if type(v) == "table" then
            print(k)
            for k, v in v do
                print(" ", k, v)
            end
        else
            print(k, v)
        end
    end
end

LLEvents:on("http_response", function(request_id, status, metadata, body)
    if status == 200 then
        receive(body)
    else
        print("Request failed.")
    end
end)

send()

The received JSON:

{
  "args": {},
  "data": {
    "value": 123,
    "task": "test",
    "object_name": "JSON testing object"
  },
  "files": {},
  "form": {},
  "headers": {
    "host": "postman-echo.com",
    "x-secondlife-local-velocity": "(0.000000, 0.000000, 0.000000)",
    "accept-encoding": "gzip, br",
    "x-secondlife-local-position": "(104.017860, 190.604568, 22.250095)",
    "x-forwarded-proto": "https",
    "content-type": "application/json",
    "x-secondlife-region": "SLua Beta Nicoise (255744, 253952)",
    "x-secondlife-local-rotation": "(0.000000, 0.000000, 0.000000, 1.000000)",
    "x-secondlife-object-key": "db02d587-2bf9-3322-007b-5d80cf3f43fc",
    "x-secondlife-owner-key": "0f16c0e1-384e-4b5f-b7ce-886dda3bce41",
    "accept": "text/*, application/xhtml+xml, application/atom+xml, application/json, application/xml, application/llsd+xml, application/x-javascript, application/javascript, application/x-www-form-urlencoded, application/rss+xml",
    "content-length": "63",
    "x-secondlife-shard": "Production",
    "cache-control": "no-cache, max-age=0",
    "accept-charset": "utf-8;q=1.0, *;q=0.5",
    "user-agent": "Second-Life-LSL/2026-01-06.20757451310 (https://secondlife.com)",
    "x-secondlife-owner-name": "SuzannaLinn Resident",
    "pragma": "no-cache",
    "x-secondlife-object-name": "JSON testing object"
  },
  "json": {
    "value": 123,
    "task": "test",
    "object_name": "JSON testing object"
  },
  "url": "https://postman-echo.com/post"
}

metamethod __tojson

When there is a metamethod __tojson, lljson.encode() calls it and uses the returned data to generate the JSON representation of the table, instead of reading the table.

It’s useful to adapt the contents of the table to the format that the external language or website expects:

-- exporting a table formatted with __tojson
local fruitsQuantity = { Apple = 50, Banana = 30, Cherry = 20, Orange = 15 }
print(lljson.encode(fruitsQuantity))
-- > {"Apple":50,"Cherry":20,"Orange":15,"Banana":30}
	
local fruitsQuantity_mt = {
    __tojson = function(t)
        local jsonFruits = { title = "List of fruits and quantities", total = 0, fruits = {} }
        for k, v in t do
            table.insert(jsonFruits.fruits, { name = k, quantity = v })
            jsonFruits.total += v
        end
        return jsonFruits
    end
}
setmetatable(fruitsQuantity, fruitsQuantity_mt)
print(lljson.encode(fruitsQuantity))
-- > {"fruits":[{"name":"Apple","quantity":50},{"name":"Cherry","quantity":20},{"name":"Orange","quantity":15},{"name":"Banana","quantity":30}],"total":115,"title":"List of fruits and quantities"}

Generated JSON ready to export:

{
  "fruits": [
    {
      "name": "Apple",
      "quantity": 50
    },
    {
      "name": "Cherry",
      "quantity": 20
    },
    {
      "name": "Orange",
      "quantity": 15
    },
    {
      "name": "Banana",
      "quantity": 30
    }
  ],
  "total": 115,
  "title": "List of fruits and quantities"
}

Anything that we return in __tojson will be generated instead of the table:

-- exporting a table formated with __tojson
local fruitsQuantity = { Apple = 50, Banana = 30, Cherry = 20, Orange = 15 }
local fruitsQuantity_mt = {
    __tojson = function(t)
        return "non exportable table"
    end
}
setmetatable(fruitsQuantity, fruitsQuantity_mt)
print(lljson.encode(fruitsQuantity))
-- > "non exportable table"

We can export a sparse array as a JSON object, avoiding the “Cannot serialise table: excessively sparse array” error:

-- sparse array to JSON object with __tojson
local tab = {}
tab[25], tab[50] = "value 25", "value 50"
local mt = {
    __tojson = function(t)
        local jsonTab = {}
        for k, v in t do
            jsonTab[tostring(k)] = v
        end
        return jsonTab
    end
}
setmetatable(tab, mt)
print(lljson.encode(tab))
-- > {"25":"value 25","50":"value 50"}

We can improve the previous script for a more general use to export proper arrays as JSON arrays and sparse arrays as JSON objects.
The idea is that if the array isn’t sparse the metamethod will return the unchanged table.
But it can return the same table, because lljson.encode() would call __tojson on it and go into infinite recursion, until throwing the error “Cannot serialise, excessive nesting (101)”.
It returns a cloned table (cloned along with its metatable) with its metatable subsequently set to nil:

-- array to JSON array and sparse array to JSON object with __tojson
local vegetables = { "Carrot", "Tomato", "Potato", "Onion", "Lettuce" }
local mt = {
    __tojson = function(t)
        if table.maxn(t) > #t then
            local jsonVegetables = {}
            for k, v in t do
                jsonVegetables[tostring(k)] = v
            end
            return jsonVegetables
        else
			-- return t  -- WRONG! __tojson is called again and goes into infinite recursion
			-- return table.clone(t)  -- WRONG! the table is cloned with its metatable
            return setmetatable(table.clone(t), nil)
        end
    end
}
setmetatable(vegetables, mt)
print(lljson.encode(vegetables))
-- > ["Carrot","Tomato","Potato","Onion","Lettuce"]

We can typecast keys to string, avoiding the “Cannot serialise userdata: table key must be a number or string” error:

-- dictionary keys as string to JSON object with __tojson
local tab = { [ll.GetOwner()] = true }
local mt = {
    __tojson = function(t)
        local jsonTab = {}
        for k, v in t do
            jsonTab[tostring(k)] = v
        end
        return jsonTab
    end
}
setmetatable(tab, mt)
print(lljson.encode(tab))
-- > {"0f16c0e1-384e-4b5f-b7ce-886dda3bce41":true}

metamethod __len

When there is a __len metamethod, lljson.encode() generates an array with the array part of the table and the length returned by __len. It uses null for the nil indexes, and adds nulls at the end if there are not enough elements:

-- table to JSON array with __len length
local tab = { 1, 2, 3, a = "a", b = "b" }
local mt = {
	__len = function(t)  -- nonsense function for testing
		return math.random(0, 10)
	end
}
setmetatable(tab, mt)
print(lljson.encode(tab))
-- > [1,2,3,null]
print(lljson.encode(tab))
-- > [1,2]
print(lljson.encode(tab))
-- > [1,2,3,null,null,null,null]

There is no limit to the quantity of nulls used.

We can export a sparse array as a JSON array, avoiding the “Cannot serialise table: excessively sparse array” error:

-- excessively sparse array to JSON array with __len
local tab = {}
tab[25], tab[50] = "value 25", "value 50"
local mt = { 
	__len = function(t)
		return table.maxn(t)
	end
}
setmetatable(tab, mt)
print(lljson.encode(tab))
-- > [null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,
-- > null,null,null,null,"value 25",null,null,null,null,null,null,null,null,null,null,null,null,null,null,
-- > null,null,null,null,null,null,null,null,null,null,"value 50"]

But we need to be careful if we are using __len for something else:

 -- dictionary table to JSON object generates array of nulls with __len
local tab = { a = 1, b = 2, c = 3 }
local mt = { 
	__len = function(t)
		local len = 0 
		for _ in t do 
			len += 1 
		end 
		return len 
	end
}
setmetatable(tab, mt)
print(lljson.encode(tab))
-- > [null,null,null]

The solution is to use __tojson to return the same table without its metatable:

-- dictionary table to JSON object with __tojson to avoid __len
local tab = { a = 1, b = 2, c = 3 }
local mt = {
	__len = function(t)
		local len = 0 
		for _ in t do 
			len += 1 
		end 
		return len 
	end,
    __tojson = function(t)
		return setmetatable(table.clone(t), nil) 
	end
}
setmetatable(tab, mt)
print(lljson.encode(tab))
-- > {"a":1,"c":3,"b":2}

metamethod __index

The metamethod __index is called in the usual way when lljson.encode() reads a nil array index.

The check for sparse arrays is done previously and we can’t use __index to avoid the “excessively sparse array” error.

We can use __index to replace nil with another value:

-- __index to use "" instead of null
local vegetables = { "Carrot", "Tomato", "Potato", "Onion", "Lettuce" }
vegetables[4] = nil
local vegetables_mt = {
    __index = function(t, k)
        return ""
    end
}
setmetatable(vegetables, vegetables_mt)
print(lljson.encode(vegetables))
-- > ["Carrot","Tomato","Potato","","Lettuce"]

With __len to get data from other tables:


-- __index and __len to merge data from several tables
local fruits = { "Apple", "Banana", "Orange" }
local fruitsColors = { Apple = "Red", Banana = "Yellow", Orange = "Orange" }
local fruitsPrices = { Apple = 1.20, Banana = 0.80, Orange = 1.50 }

local fruits_mt = {
    __index = function(t, k)
        local fruit = fruits[k]
        return { fruit = fruit, color = fruitsColors[fruit], price = fruitsPrices[fruit] }
    end,
    __len = function(t)
        return #fruits
    end
}

print(lljson.encode(setmetatable({}, fruits_mt)))
--> [{"color":"Red","fruit":"Apple","price":1.2},{"color":"Yellow","fruit":"Banana","price":0.8},{"color":"Orange","fruit":"Orange","price":1.5}]

With __tojson and __len to generate new data. Using __tojson to get parameters, __index as an iterator and __len as the limit of the iteration:

-- __tojson, __index and __len to generate calculated JSON
local mt = {
    __tojson = function(t)
        return setmetatable({}, {
            __index = (function()
                local a, b = 1, 1
                local count = 0
                return function()
                    local res = a
                    a, b = b, a + b 
                    count += 1
                    return res
                end
            end)(),
            __len = function()
                return t[1]
            end
        })
    end
}

print(lljson.encode(setmetatable({ 15 }, mt)))
-- > [1,1,2,3,5,8,13,21,34,55,89,144,233,377,610]

lljson constants

Asides from the 4 functions, there are 6 constants to give instructions for encoding and for information.

null and empty_array

They are two values that can’t be represented with SLua values.

lljson.null : encodes to a JSON null. Useful to add null keys to a JSON object.

-- lljson.null as null JSON key
local animals = { { kind = "dog", name = "Dufa", color = "white", wingspan = lljson.null } }
print(lljson.encode(animals))
-- > [{"color":"white","kind":"dog","name":"Dufa","wingspan":null}]

lljson.empty_array : encodes a JSON empty array (instead of a JSON empty object, that is the default for an empty table).

-- lljson.empty_array as empty JSON array
local animals = { { kind = "dog", name = "Dufa", color = "white", puppies = lljson.empty_array } }
print(lljson.encode(animals))
-- > [{"color":"white","kind":"dog","name":"Dufa","puppies":[]}]

They have type “lljson_constant” derived from “userdata” (internally a value type derived from light userdata). They can’t be confused with any other value of another type:

print( type(lljson.null), typeof(lljson.null), lljson.null )
-- > userdata    lljson_constant    lljson_constant: 0x0000000000000003
print( type(lljson.empty_array), typeof(lljson.empty_array), lljson.empty_array )
-- > userdata    lljson_constant    lljson_constant: 0x0000000000000005

array_mt and empty_array_mt

They are two metatables that can be set to the table to give encoding instructions

lljson.array_mt : encodes the array part of the table as JSON array.

-- array part of mixed table to JSON array
local vegetables = { "Carrot", "Tomato", "Potato", Lettuce = "favorite" }
setmetatable(vegetables, lljson.array_mt)
print(lljson.encode(vegetables))
-- > ["Carrot","Tomato","Potato"]

lljson.empty_array_mt : if the table is empty, encodes a JSON empty array (instead of a JSON empty object, that is the default for an empty table).

-- empty table as JSON array
local tab = { "hello" }
setmetatable(tab, lljson.empty_array_mt)
print(lljson.encode(tab))
--> ["hello"]
table.remove(tab, 1)
print(lljson.encode(tab))
--> []

They are empty tables, used as markers. lljson.encode() gets the metatable and uses it as a parameter.

_NAME and _VERSION

They are two string constants with the name and version of the library:

print(lljson._NAME)
--> lljson
print(lljson._VERSION)
--> 2.1.0.11

slencode() / sldecode()

These functions work with non standard JSON-like code that can be encoded and decoded as the same table. They are useful to exchange code with other scripts or objects or to store in linkset data:

-- encoding to internal JSON ready to be decoded unchanged
local tab = { 42, 3.14, "hello", true, ll.GetOwner(), vector(25, 50, 0), rotation(0.50, 0.25, 0, 1), "!vStringInVectorDisguise" }
local s = lljson.slencode(tab)
print(s)
-- > [42,3.14,"hello",true,"!u0f16c0e1-384e-4b5f-b7ce-886dda3bce41","!v<25,50,0>","!q<0.5,0.25,0,1>",
-- > "!!vStringInVectorDisguise"]
local tabs = lljson.sldecode(s)
for _, v in tabs do
    print(typeof(v), v)
end
-- > number     42
-- > number     3.14
-- > string     hello
-- > boolean    true
-- > uuid       0f16c0e1-384e-4b5f-b7ce-886dda3bce41
-- > vector     <25, 50, 0>
-- > quaternion <0.5, 0.25, 0, 1>
-- > string     !vStringInVectorDisguise

To send JSON data to external resources use encode(). To receive JSON data from external resources use decode().

The generated code doesn’t contain any special character so we can store it safely in linkset data.

A table’s metatable can’t be encoded. We have to set it again after decoding the table. If it is decoded in another script, the metatable has to be defined there too.

issue : nils are decoded as lljson.null. This will be resolved. issue : lljson.empty_array is decoded as an empty table. This will be resolved.

slencode() uses the metamethods __tojson and __len and the metatables lljson.array_mt and lljson.empty_array_mt, like encode().
We can use them to export to an external resource and also be able to use it internally:

-- encoding for external and internal use
local slEncode
local fruits = { "apples", "bananas", "oranges" }
local fruits_mt = {
    __tojson = function(t)
        if slEncode then
            return setmetatable(table.clone(t), nil)
        end
        local jsonFruits = {}
        for _, v in t do
            table.insert(jsonFruits, v:upper())
        end
        return jsonFruits
    end
}
setmetatable(fruits, fruits_mt)
slEncode = false
print(lljson.encode(fruits))  -- external use
-- > ["APPLES","BANANAS","ORANGES"]  -- ready to export
slEncode = true
print(lljson.slencode(fruits))  -- internal use
-- > ["apples","bananas","oranges"]  -- ready to be decoded

If the table has a __len metamethod we have to add a __tojson to return the table without the metatable:

-- using __tojson to avoid __len
local tab = { a = 1, b = 2, c = 3 }
local mt = {
	__len = function(t)
		local len = 0 
		for _ in t do 
			len += 1 
		end 
		return len 
	end,
    __tojson = function(t)
		return setmetatable(table.clone(t), nil) 
	end
}
setmetatable(tab, mt)
print(lljson.slencode(tab))
-- > {"a":1,"c":3,"b":2}

possible improvement : It is requested that slencode() not use metatables and metamethods.

Tight encoding

slencode() has a second parameter for tight encoding, false by default. With true some data types are encoded with less characters.

Changes are:

  • vectors and rotations: encoded without “<” and “>” and coordinates with value 0 as empty.
  • uuids : encoded in numeric format as base64 strings in 22 characters instead of 36 (plus the 2 characters of the “!u” tag).
-- encoding tight
local tab = { ll.GetOwner(), vector(25, 50, 0), rotation(0.50, 0.25, 0, 1) }
print(lljson.slencode(tab))
-- > ["!u0f16c0e1-384e-4b5f-b7ce-886dda3bce41","!v<25,50,0>","!q<0.5,0.25,0,1>"]
print(lljson.slencode(tab, true))
-- > ["!uDxbA4ThOS1+3zoht2jvOQQ","!v25,50,","!q0.5,0.25,,1"]

sldecode() identifies both formats automatically, so we don’t have to specify the format.

Codes

We don’t need to know how the internal JSON is encoded; slencode() and sldecode() manage it so we don’t have to worry about it.

The encoding is important only if we are scripting an external resource in another language that reads slencode() format and/or writes sldecode() format.

Encoding of data types, some of them change depending on wether they are used as a value or as a key:

SLua type Encoding Comments
string string if it starts with "!" then "!" .. string
number (value) number
number (key) "!f" string
boolean (value) boolean
boolean (key) "!b1" : true, "!b0" : false
table (value) array [] or object {}
table (key) not allowed
vector "!v" string "!v<25,50,0>"
vector (tight) "!v" string "!v25,50,"
rotation/quaternion "!q" string "!q<0.5,0.25,0,1>"
rotation/quaternion (tight) "!q" string "!q0.5,0.25,,1"
uuid "!u" string
uuid (tight) "!u" base64 string 22 chars (see note below)
buffer "!d" base64 string
function not allowed
thread (coroutine) not allowed
nil (value) null encode will change, now decodes to lljson.null
nil (key) not possible in SLua
lljson.null (value) null
lljson.null (key) not allowed
lljson.empty_array (value) [] encode will change, now decodes to {}
lljson.empty_array (key) not allowed
inf, -inf (value) 1e9999, -1e9999
inf, -inf (key) "!f1e9999", "!f-1e9999"
nan (value) NaN (literal, not string) encode could change, now is non-standard JSON
nan (key) not possible in SLua

Encoding of uuid (tight)

A uuid in base64 consists of 24 characters, but only the first 22 are significant. The final two characters are padding and are not encoded:

-- uuid with tight encoding

local id = ll.GetOwner()

local json = lljson.slencode(id, true)
print(json)  -- > "!uDxbA4ThOS1+3zoht2jvOQQ"

local idEncoded = '"' .. "!u" .. llbase64.encode(id.bytes):sub(1, 22) .. '"'
print(idEncoded == json)  -- > true

local idDecoded = uuid(buffer.fromstring(llbase64.decode(idEncoded:sub(4, 25) .. "==")))
print(idDecoded == id)  -- > true

An empty uuid is encoded as “!u”:

-- empty uuid with tight encoding
local id = NULL_KEY
print(lljson.slencode(id, true))  -- > "!u"