Module:JSON

Hello, you have come here looking for the meaning of the word Module:JSON. In DICTIOUS you will not only get to know all the dictionary meanings for the word Module:JSON, but we will also tell you about its etymology, its characteristics and you will know how to say Module:JSON in singular and plural. Everything you need to know about the word Module:JSON you have here. The definition of the word Module:JSON will help you to be more precise and correct when speaking or writing your texts. Knowing the definition ofModule:JSON, as well as those of other words, enriches your vocabulary and provides you with more and better linguistic resources.

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

(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",
	 = "\\t",
	 = "\\n",
	 = "\\f",
	 = "\\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 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, '', 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, '', 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 = 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 then
				error(format("Collision for JSON key %s", key), level)
			end
			seen_keys = 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 = 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 then
		error("Cannot use recursive tables", level)
	end
	-- Memoize the table to enable recursion checking.
	current = 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 = 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 = 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