Table Serialization


Here are functions to serialize/unserialize a table or object (usually, not always, represented as a table), which is to convert it to and from a string representation. This is typically used for display (e.g. debugging) or storing data in a file (e.g. persistence).

Design decisions include

Because of these different needs, there have been many implementations.

Implementations

Identity-preserving table serialization by Metalua

This example doesn't focus on the readability of the serialized table. Instead, and contrary to other examples in this page, it conserves shared sub-structures. Here's an example of a test that would be passed by metalua serialization, but not by pretty-printers:

> x={ 1 }
> x[2] = x
> x[x] = 3
> x[3]={ 'indirect recursion', [x]=x }
> y = { x, x }
> x.y = y
> assert (y[1] == y[2])
> s = serialize (x)
> z = loadstring (s)()
> assert (z.y[1] == z.y[2])
> =s
local _={ }
_[1]={ "indirect recursion" }
_[2]={ false, false }
_[3]={ 1, false, _[1], ["y"] = _[2] }
_[3][2] = _[3]
_[1][_[3]] = _[3]
_[3][_[3]] = 3
_[2][1] = _[3]
_[2][2] = _[3]
return _[3]
>

Sources for this serializer are available on the MetaLua repository: [2].

Metalua table.tostring and table.print

These functions are intended for pretty-printing rather than serialization: they don't preserve identity. They will terminate, though: if a table references itself, the inner occurrence will be printed as "[table: 0x12345678]" in order to avoid infinite recursion.

require "table2"
require "string2"
local u = {9}
local t = {2, "3\"4", {5, 6}, x=function() end, [u]=u}
table.print(t)
--> { [{ 9 }] = { 9 }, x = function: 0x6a2870, 2, "3\"4", { 5, 6 } }
table.print(t, 'nohash')
--> { 2, "3\"4", { 5, 6 } }
table.print(t, 'nohash', 10)
--> { 2,
-- "3\"4",
-- { 5, 6 } }
-- The `tag' field is particularly important in metalua, to represent tree-like structures.
-- As such, it has got a special syntax, introduced by a back-quote "`",
-- which is rendered by default by metalua's pretty printers.
local t = {tag='Sum', 1, {tag='Product', 2, 3}, lines={10,11}}
table.print(t)
--> `Sum{ lines = { 10, 11 }, 1, `Product{ 2, 3 } } -- metalua tag syntax
table.print(t, 'nohash')
--> `Sum{ 1, `Product{ 2, 3 } }
table.print(t, 'nohash', 10) -- metalua tag syntax
--> `Sum{ 1,
-- `Product{ 2,
-- 3 } }
-- tags syntax can be disabled:
table.print(t, 'nohash', 'notag')
--> { tag = "Sum", 1, { tag = "Product", 2, 3 } } -- tag syntax disabled
-- Note: table.print(t, ...) is equivalent to print(table.tostring(t, ...)).

Print a table recursively

ISSUE: this should return a string rather than assume how the user wants to output the text.

-- Print anything - including nested tables
function table_print (tt, indent, done)
 done = done or {}
 indent = indent or 0
 if type(tt) == "table" then
 for key, value in pairs (tt) do
 io.write(string.rep (" ", indent)) -- indent it
 if type (value) == "table" and not done [value] then
 done [value] = true
 io.write(string.format("[%s] => table\n", tostring (key)));
 io.write(string.rep (" ", indent+4)) -- indent it
 io.write("(\n");
 table_print (value, indent + 7, done)
 io.write(string.rep (" ", indent+4)) -- indent it
 io.write(")\n");
 else
 io.write(string.format("[%s] => %s\n",
 tostring (key), tostring(value)))
 end
 end
 else
 io.write(tt .. "\n")
 end
end

Universal tostring

function table_print (tt, indent, done)
 done = done or {}
 indent = indent or 0
 if type(tt) == "table" then
 local sb = {}
 for key, value in pairs (tt) do
 table.insert(sb, string.rep (" ", indent)) -- indent it
 if type (value) == "table" and not done [value] then
 done [value] = true
 table.insert(sb, key .. " = {\n");
 table.insert(sb, table_print (value, indent + 2, done))
 table.insert(sb, string.rep (" ", indent)) -- indent it
 table.insert(sb, "}\n");
 elseif "number" == type(key) then
 table.insert(sb, string.format("\"%s\"\n", tostring(value)))
 else
 table.insert(sb, string.format(
 "%s = \"%s\"\n", tostring (key), tostring(value)))
 end
 end
 return table.concat(sb)
 else
 return tt .. "\n"
 end
end
function to_string( tbl )
 if "nil" == type( tbl ) then
 return tostring(nil)
 elseif "table" == type( tbl ) then
 return table_print(tbl)
 elseif "string" == type( tbl ) then
 return tbl
 else
 return tostring(tbl)
 end
end

Example

print(to_string{
 "Lua",user="Mariacher",
 {{co=coroutine.create(function() end),{number=12345.6789}},
 func=function() end}, boolt=true} )

This prints

"Lua"
{
 {
 {
 number = "12345.6789"
 }
 co = "thread: 0212B848"
 }
 func = "function: 01FC7C70"
}
boolt = "true"
user = "Mariacher"

(the above code was originally from TableUtils)

PHP-like print_r

Based on [PHP print_r]. Based on code by Nick Gammon, hacked by DracoBlue to fit to [PHP print_r]-Style.

Example: print_r{ 5,3,{5,3} } -->

[1] => 5
[2] => 3
[3] => Table 
 {
 [1] => 5
 [2] => 3
 }

Compatibility: Lua 5.0 and 5.1

function print_r (t, indent, done)
 done = done or {}
 indent = indent or ''
 local nextIndent -- Storage for next indentation value
 for key, value in pairs (t) do
 if type (value) == "table" and not done [value] then
 nextIndent = nextIndent or
 (indent .. string.rep(' ',string.len(tostring (key))+2))
 -- Shortcut conditional allocation
 done [value] = true
 print (indent .. "[" .. tostring (key) .. "] => Table {");
 print (nextIndent .. "{");
 print_r (value, nextIndent .. string.rep(' ',2), done)
 print (nextIndent .. "}");
 else
 print (indent .. "[" .. tostring (key) .. "] => " .. tostring (value).."")
 end
 end
end
function print_r (t, indent) -- alt version, abuse to http://richard.warburton.it
 local indent=indent or ''
 for key,value in pairs(t) do
 io.write(indent,'[',tostring(key),']') 
 if type(value)=="table" then io.write(':\n') print_r(value,indent..'\t')
 else io.write(' = ',tostring(value),'\n') end
 end
end
-- alt version2, handles cycles, functions, booleans, etc
-- - abuse to http://richard.warburton.it
-- output almost identical to print(table.show(t)) below.
function print_r (t, name, indent)
 local tableList = {}
 function table_r (t, name, indent, full)
 local serial=string.len(full) == 0 and name
 or type(name)~="number" and '["'..tostring(name)..'"]' or '['..name..']'
 io.write(indent,serial,' = ') 
 if type(t) == "table" then
 if tableList[t] ~= nil then io.write('{}; -- ',tableList[t],' (self reference)\n')
 else
 tableList[t]=full..serial
 if next(t) then -- Table not empty
 io.write('{\n')
 for key,value in pairs(t) do table_r(value,key,indent..'\t',full..serial) end 
 io.write(indent,'};\n')
 else io.write('{};\n') end
 end
 else io.write(type(t)~="number" and type(t)~="boolean" and '"'..tostring(t)..'"'
 or tostring(t),';\n') end
 end
 table_r(t,name or '__unnamed__',indent or '','')
end

Here is a more complete version of print_r:

Sorry for the length!

--[[
 Author: Julio Manuel Fernandez-Diaz
 Date: January 12, 2007
 (For Lua 5.1)
 
 Modified slightly by RiciLake to avoid the unnecessary table traversal in tablecount()
 Formats tables with cycles recursively to any depth.
 The output is returned as a string.
 References to other tables are shown as values.
 Self references are indicated.
 The string returned is "Lua code", which can be procesed
 (in the case in which indent is composed by spaces or "--").
 Userdata and function keys and values are shown as strings,
 which logically are exactly not equivalent to the original code.
 This routine can serve for pretty formating tables with
 proper indentations, apart from printing them:
 print(table.show(t, "t")) -- a typical use
 
 Heavily based on "Saving tables with cycles", PIL2, p. 113.
 Arguments:
 t is the table.
 name is the name of the table (optional)
 indent is a first indentation (optional).
--]]
function table.show(t, name, indent)
 local cart -- a container
 local autoref -- for self references
 --[[ counts the number of elements in a table
 local function tablecount(t)
 local n = 0
 for _, _ in pairs(t) do n = n+1 end
 return n
 end
 ]]
 -- (RiciLake) returns true if the table is empty
 local function isemptytable(t) return next(t) == nil end
 local function basicSerialize (o)
 local so = tostring(o)
 if type(o) == "function" then
 local info = debug.getinfo(o, "S")
 -- info.name is nil because o is not a calling level
 if info.what == "C" then
 return string.format("%q", so .. ", C function")
 else 
 -- the information is defined through lines
 return string.format("%q", so .. ", defined in (" ..
 info.linedefined .. "-" .. info.lastlinedefined ..
 ")" .. info.source)
 end
 elseif type(o) == "number" or type(o) == "boolean" then
 return so
 else
 return string.format("%q", so)
 end
 end
 local function addtocart (value, name, indent, saved, field)
 indent = indent or ""
 saved = saved or {}
 field = field or name
 cart = cart .. indent .. field
 if type(value) ~= "table" then
 cart = cart .. " = " .. basicSerialize(value) .. ";\n"
 else
 if saved[value] then
 cart = cart .. " = {}; -- " .. saved[value] 
 .. " (self reference)\n"
 autoref = autoref .. name .. " = " .. saved[value] .. ";\n"
 else
 saved[value] = name
 --if tablecount(value) == 0 then
 if isemptytable(value) then
 cart = cart .. " = {};\n"
 else
 cart = cart .. " = {\n"
 for k, v in pairs(value) do
 k = basicSerialize(k)
 local fname = string.format("%s[%s]", name, k)
 field = string.format("[%s]", k)
 -- three spaces between levels
 addtocart(v, fname, indent .. " ", saved, field)
 end
 cart = cart .. indent .. "};\n"
 end
 end
 end
 end
 name = name or "__unnamed__"
 if type(t) ~= "table" then
 return name .. " = " .. basicSerialize(t)
 end
 cart, autoref = "", ""
 addtocart(t, name, indent)
 return cart .. autoref
end

A test:

-----------------------------------------------------------
--- testing table.show
t = {1, {2, 3, 4}, default = {"a", "b", d = {12, "w"}, e = 14}}
t.g = t.default
print("-----------------------------------")
print(table.show(t)) -- shows __unnamed__ table
tt = {1, h = {["p-q"] = "a", b = "e", c = {color = 3, name = "abc"}}, 2}
f = table.show
tt[f] = "OK"
print("-----------------------------------")
print(table.show(tt, "tt", "--oo-- ")) -- shows some initial 'indent'
t.m = {}
t.g.a = {}
t.g.a.c = t
t.tt = tt.new
t.show = table.show
print("-----------------------------------")
print(table.show(t, "t")) -- most typical use
print("-----------------------------------")
print(table.show(math.tan, "tan")) -- not a table is OK
print("-----------------------------------")
s = "a string"
print(table.show(s, "s")) -- not a table is OK

The output:

-----------------------------------
__unnamed__ = {
 [1] = 1;
 [2] = {
 [1] = 2;
 [2] = 3;
 [3] = 4;
 };
 ["default"] = {
 [1] = "a";
 [2] = "b";
 ["e"] = 14;
 ["d"] = {
 [1] = 12;
 [2] = "w";
 };
 };
 ["g"] = {}; -- __unnamed__["default"] (self reference)
};
__unnamed__["g"] = __unnamed__["default"];
-----------------------------------
--oo-- tt = {
--oo-- [1] = 1;
--oo-- [2] = 2;
--oo-- ["function: 0x8070e20, defined in (28-99)@newprint_r.lua"] = "OK";
--oo-- ["h"] = {
--oo-- ["b"] = "e";
--oo-- ["c"] = {
--oo-- ["color"] = 3;
--oo-- ["name"] = "abc";
--oo-- };
--oo-- ["p-q"] = "a";
--oo-- };
--oo-- };
-----------------------------------
t = {
 [1] = 1;
 [2] = {
 [1] = 2;
 [2] = 3;
 [3] = 4;
 };
 ["m"] = {};
 ["show"] = "function: 0x8070e20, defined in (28-99)@newprint_r.lua";
 ["g"] = {
 [1] = "a";
 [2] = "b";
 ["e"] = 14;
 ["d"] = {
 [1] = 12;
 [2] = "w";
 };
 ["a"] = {
 ["c"] = {}; -- t (self reference)
 };
 };
 ["default"] = {}; -- t["g"] (self reference)
};
t["g"]["a"]["c"] = t;
t["default"] = t["g"];
-----------------------------------
tan = "function: 0x806f758, C function"
-----------------------------------
s = "a string"

(the above code originally existed in MakingLuaLikePhp)

Warning: the above does not work properly as shown here:

x = {1, 2, 3}
x[x]=x
print(table.show(x))
--[[output:
__unnamed__ = {
 [1] = 1;
 [2] = 2;
 [3] = 3;
 ["table: 0x695f08"] = {}; -- __unnamed__ (self reference)
};
__unnamed__["table: 0x695f08"] = __unnamed__;
--]]

Dump any object including tables with identifiable self-references into a string

local val_to_str; do
 -- Cached function references (for performance).
 local byte = string.byte
 local find = string.find
 local match = string.match
 local gsub = string.gsub
 local format = string.format
 local insert = table.insert
 local sort = table.sort
 local concat = table.concat
 -- For escaping string values.
 local str_escape_map = {
 ['\a'] = '\\a', ['\b'] = '\\b', ['\t'] = '\\t', ['\n'] = '\\n',
 ['\v'] = '\\v', ['\f'] = '\\f', ['\r'] = '\\r', ['\\'] = '\\\\' }
 local str_escape_replace = function(c)
 return str_escape_map[c] or format('\\%03d', byte(c))
 end
 -- Keys are comparable only if the same type, otherwise just sort them by type.
 local types_order, ref_types_order = {
 ['number'] = 0, ['boolean'] = 1, ['string'] = 2, ['table'] = 3,
 ['function'] = 4 }, 5
 end
 local function compare_keys(k1, k2)
 local t1, t2 = type(k1), type(k2)
 if t1 ~= t2 then -- not the same type
 return (types_order[t1] or ref_types_order)
 < (types_order[t2] or ref_types_order)
 elseif t1 == 'boolean' then -- comparing booleans
 return not k1 -- Sort false before true.
 elseif t1 == 'number' or t1 == 'string' then -- comparing numbers (including NaNs or infinites) or strings
 return k1 < k2 -- Keys with the same comparable type.
 else -- comparing references (including tables, functions, userdata, threads...)
 return tostring(k1) < tostring(k2) -- may be the Lua engine adds some comparable info
 end
 end
 -- String keys matching valid identifiers that are reserved by Lua.
 local reserved_keys = {
 ['and'] = 1, ['break'] = 1, ['do'] = 1, ['else'] = 1,
 ['elseif'] = 1, ['end'] = 1, ['false'] = 1, ['for'] = 1,
 ['function'] = 1, ['if'] = 1, ['in'] = 1, ['local'] = 1,
 ['nil'] = 1, ['not'] = 1, ['or'] = 1, ['repeat'] = 1,
 ['return'] = 1, ['then'] = 1, ['true'] = 1, ['until'] = 1,
 ['while'] = 1 }
 -- Main function.
 val_to_str = function(val, options)
 -- Decode and cache the options.
 local include_mt = options and options.include_mt
 local prettyprint = options and options.prettyprint
 local asciionly = options and options.asciionly
 -- Precompute the output formats depending on options.
 local open = prettyprint and '{ ' or '{'
 local equals = prettyprint and ' = ' or '='
 local comma = prettyprint and ', ' or ','
 local close = prettyprint and ' }' or '}'
 -- What to escape: C0 controls, the backslash, and optionally non-ASCII bytes.
 local str_escape_pattern = asciionly and '[%z001円-031円\\127円-255円]' or '[%z001円-031円\\127円]'
 -- Indexed references (mapped to ids), and counters per ref type.
 local ref_ids, ref_counts = {}, {}
 -- Helper needed to detect recursive tables and avoid infinite loops.
 local function visit(ref)
 local typ = type(ref)
 if typ == 'number' or typ == 'boolean' then
 return tostring(ref)
 elseif typ == 'string' then
 if find(ref, "'") then
 str_escape_map['"'] = '\\"'
 return '"' .. gsub(ref, str_escape_pattern, str_escape_replace) .. '"'
 else
 str_escape_map['"'] = '"'
 return "'" .. gsub(ref, str_escape_pattern, str_escape_replace) .. "'"
 end
 elseif typ == 'table' then
 	 local id = ref_ids[ref]
 if id then
 return ':' .. typ .. '#' .. id .. ':'
 end
 id = (ref_counts[typ] or 0) + 1; ref_ids[ref], ref_counts[typ] = id, id
 -- First dump keys that are in sequence.
 local result, sequenced, keys = {}, {}, {}
 for i, val in ipairs(ref) do
 insert(result, visit(val))
 sequenced[i] = true
 end
 -- Then dump other keys out of sequence, in a stable order.
 for key, _ in pairs(ref) do
 if not sequenced[key] then
 insert(keys, key)
 end
 end
 sequenced = nil -- Free the temp table no longer needed.
 -- Sorting keys (of any type) is needed for stable comparison of results.
 sort(keys, compare_keys)
 for _, key in ipairs(keys) do
 insert(result,
 (type(key) == 'string' and
 not reserved_keys[key] and match(key, '^[%a_][%d%a_]*$') and
 key or '[' .. visit(key) .. ']') ..
 equals .. visit(ref[key]))
 end
 keys = nil -- Free the temp table no longer needed.
 -- Finally dump the metatable (with pseudo-key '[]'), if there's one.
 if include_mt then
 ref = getmetatable(ref)
 if ref then
 insert(result, '[]' .. equals .. visit(ref))
 end
 end
 -- Pack the result string.
 -- TODO: improve pretty-printing with newlines/indentation
 return open .. concat(result, comma) .. close
 elseif typ ~= 'nil' then -- other reference types (function, userdata, etc.)
 local id = ref_ids[ref]
 if not id then
 id = (ref_counts[typ] or 0) + 1; ref_ids[ref], ref_counts[ref] = id, id
 end
 return ':' .. typ .. '#' .. id .. ':'
 else
 return 'nil'
 end
 end
 return visit(val)
 end
end

Example usage:

 local s = val_to_str(anyvalue)
 local s = val_to_str(anyvalue, {include_mt=1, prettyprint=1}) -- you can specify optional flags

Notes:


RecentChanges · preferences
edit · history
Last edited September 5, 2021 4:54 am GMT (diff)

AltStyle によって変換されたページ (->オリジナル) /