Module:JSON
Appearance
- The following documentation is located at Module:JSON/documentation. [edit]
- Useful links: subpage list • links • transclusions • testcases • sandbox
This module offers some utility methods for converting Lua values into JSON values (in UTF-8-encoded Lua strings).
Unfortunately, Lua's data model differs somewhat from JSON's, so it's not possible to write a general function that takes any Lua value and returns a JSON value, always "doing the right thing". Rather, some values cannot be converted at all, and other values have multiple possible non-equivalent representations.
The differences are:
- Lua has three types with no JSON analogues, namely function, userdata, and thread, so this module has no support for values of those types.
- Lua's concept of "metatables" has no analogue in JSON, so this module ignores metatables completely.
- Lua's number type, as implemented in Scribunto, consists of double-precision floating-point values, whereas JSON's number type consists of decimal representations. (And the end-recipient of the JSON data will likely convert the values back into some sort of floating-point notation.) This means that, aside from integers, you can't generally expect values to be converted exactly. (And even with integers, you can only expect perfect conversion in the range ±109 or so.) What's more, it means that Lua has a few numeric values with no JSON analogues at all, namely positive infinity, negative infinity, and "not a number" values; so, this module does not support those values.
- Lua's string type represents strings of eight-bit bytes, whereas JSON's *string* type represents strings of Unicode characters. This module requires the Lua strings to be valid UTF-8 sequences.
- Whereas Lua has only a single table type mapping from arbitrary non-nil values to arbitrary non-nil values, JSON has separate array and object types, where an array maps from a set of integers {0,1,…,n} to arbitrary values, and an object maps from arbitrary strings to arbitrary values. As a result, this module [TBD]
(Note: the above is an attempt at an exhaustive list of differences, but it's quite possible that I missed some.)
local export = {}
local m_table = require("Module:table")
local codepoint = require("Module:string utilities").codepoint
local concat = table.concat
local converter -- forward declaration
local format = string.format
local getmetatable = getmetatable
local index_ipairs = m_table.indexIpairs
local insert = table.insert
local is_array = m_table.isArray
local is_finite_real_number = require("Module:math").is_finite_real_number
local is_utf8 = mw.ustring.isutf8
local pairs = pairs
local pcall = pcall
local sorted_pairs = m_table.sortedPairs
local type = type
local ugsub = mw.ustring.gsub
-- Given a finite real number x, returns a string containing its JSON
-- representation, with enough precision that it *should* round-trip correctly
-- (depending on the well-behavedness of the system on the other end).
local function json_fromNumber(x, level)
if is_finite_real_number(x) then
return format("%.17g", x)
end
error(format("Cannot encode non-finite real number %g", x), level)
end
local escape_char_map = {
["\b"] = "\\b",
["\t"] = "\\t",
["\n"] = "\\n",
["\f"] = "\\f",
["\r"] = "\\r",
["\""] = "\\\"",
["\\"] = "\\\\",
}
local function escape_codepoint_utf16(c)
if c >= 0x10000 then
c = c - 0x10000
return format("\\u%04x\\u%04x", 0xD800 + (c / 1024), 0xDC00 + (c % 1024))
end
return format("\\u%04x", c)
end
local function escape_char(c)
return escape_char_map[c] or escape_codepoint_utf16(codepoint(c))
end
-- Given a string, escapes any illegal characters and wraps it in double-quotes.
-- Raises an error if the string is not valid UTF-8.
local function json_fromString(s, ascii, level)
if not is_utf8(s) then
error(format("Cannot encode non-UTF-8 string '%s'", s), level)
elseif ascii then
-- U+0080 = \194\128 in UTF-8, U+10FFFF = \244\143\191\191 in UTF-8
s = ugsub(s, '[%z\1-\31"\\\194\128-\244\143\191\191]', escape_char)
else
-- U+2029 (LINE SEPARATOR, \226\128\168 in UTF-8)
-- and U+2028 (PARAGRAPH SEPARATOR, \226\128\169 in UTF-8) are allowed
-- in JSON, but must be escaped for compatibility with JavaScript.
s = ugsub(s, '[%z\1-\31"\\\226\128\168\226\128\169]', escape_char)
end
return '"' .. s .. '"'
end
local function json_fromTable(t, opts, current, level)
local ret, open, close = {}
if is_array(t) then
for key, value in index_ipairs(t) do
ret[key] = converter(value, opts, current, level + 1) or "null"
end
open, close = "[", "]"
else
-- `seen_keys` memoizes keys already seen, to prevent collisions (e.g. 1
-- and "1").
local seen_keys, colon, ascii = {}, opts.compress and ":" or " : ", opts.ascii
for key, value in (opts.sort_keys and sorted_pairs or pairs)(t) do
local key_type = type(key)
if key_type == "number" then
key = json_fromNumber(key, level + 1)
elseif key_type ~= "string" then
error(format("Cannot use type '%s' as a table key", key_type), level)
end
key = json_fromString(key, ascii, level + 1)
if seen_keys[key] then
error(format("Collision for JSON key %s", key), level)
end
seen_keys[key] = true
insert(ret, key .. colon .. (converter(value, opts, current, level + 1) or "null"))
end
open, close = "{", "}"
end
ret = open .. (
opts.compress and concat(ret, ",") .. close or
" " .. concat(ret, ", ") .. (
#ret == 0 and "" or " "
) .. close
)
current[t] = nil
return ret
end
function converter(this, opts, current, level) -- local declared above
local val_type = type(this)
if val_type == "nil" then
return "null"
elseif val_type == "boolean" then
return this and "true" or "false"
elseif val_type == "number" then
return json_fromNumber(this, level + 1)
elseif val_type == "string" then
return json_fromString(this, opts.ascii, level + 1)
elseif val_type ~= "table" then
error(format("Cannot encode type '%s'", val_type), level)
elseif current[this] then
error("Cannot use recursive tables", level)
end
-- Memoize the table to enable recursion checking.
current[this] = true
if opts.ignore_toJSON then
return json_fromTable(this, opts, current, level + 1)
end
-- Check if a toJSON method can be used. Use the lua_table flag to get a Lua
-- table, as any options need to be applied to the output.
local to_json = this.toJSON
if to_json == nil then
return json_fromTable(this, opts, current, level + 1)
end
local to_json_type = type(to_json)
-- If it's a function, call it.
if to_json_type == "function" then
local ret = converter(to_json(this, {lua_table = true}), opts, current, level + 1)
current[this] = nil
return ret
-- If it's a table and there's a metatable, try to call it. If getmetatable
-- returns nil, there's definitely no metatable (so it can't be callable),
-- but otherwise the metatable could be protected with __metatable, so the
-- only reliable approach is to call it with pcall.
elseif to_json_type == "table" and getmetatable(to_json) ~= nil then
local success, new = pcall(to_json, this, {lua_table = true})
if success then
local ret = converter(new, opts, current, level + 1)
current[this] = nil
return ret
-- The error message will only take this exact form if it was thrown due
-- to `this` not being callable, as it will contain a traceback if
-- thrown in some other function, so raise the error if it's not a
-- match, since it's an error elsewhere.
elseif new ~= "attempt to call a table value" then
error(new)
end
-- Not a callable table.
end
-- Treat as a conventional value.
return json_fromTable(this, opts, current, level + 1)
end
-- This function makes an effort to convert an arbitrary Lua value to a string
-- containing a JSON representation of it. It's not intended to be very robust,
-- but may be useful for prototyping.
function export.toJSON(this, opts)
return converter(this, opts == nil and {} or opts, {}, 3)
end
return export