1--[[ json.lua 2 3A compact pure-Lua JSON library. 4 5This code is in the public domain: 6https://gist.github.com/tylerneylon/59f4bcf316be525b30ab 7 8The main functions are: json.stringify, json.parse. 9 10## json.stringify: 11 12This expects the following to be true of any tables being encoded: 13 * They only have string or number keys. Number keys must be represented as 14 strings in json; this is part of the json spec. 15 * They are not recursive. Such a structure cannot be specified in json. 16 17A Lua table is considered to be an array if and only if its set of keys is a 18consecutive sequence of positive integers starting at 1. Arrays are encoded like 19so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json 20object, encoded like so: `{"key1": 2, "key2": false}`. 21 22Because the Lua nil value cannot be a key, and as a table value is considered 23equivalent to a missing key, there is no way to express the json "null" value in 24a Lua table. The only way this will output "null" is if your entire input obj is 25nil itself. 26 27An empty Lua table, {}, could be considered either a json object or array - 28it's an ambiguous edge case. We choose to treat this as an object as it is the 29more general type. 30 31To be clear, none of the above considerations is a limitation of this code. 32Rather, it is what we get when we completely observe the json specification for 33as arbitrary a Lua object as json is capable of expressing. 34 35## json.parse: 36 37This function parses json, with the exception that it does not pay attention to 38\u-escaped unicode code points in strings. 39 40It is difficult for Lua to return null as a value. In order to prevent the loss 41of keys with a null value in a json string, this function uses the one-off 42table value json.null (which is just an empty table) to indicate null values. 43This way you can check if a value is null with the conditional 44`val == json.null`. 45 46If you have control over the data and are using Lua, I would recommend just 47avoiding null values in your data to begin with. 48 49--]] 50 51 52local json = {} 53 54 55-- Internal functions. 56 57local function kind_of(obj) 58 if type(obj) ~= 'table' then return type(obj) end 59 local i = 1 60 for _ in pairs(obj) do 61 if obj[i] ~= nil then i = i + 1 else return 'table' end 62 end 63 if i == 1 then return 'table' else return 'array' end 64end 65 66local function escape_str(s) 67 local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} 68 local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} 69 for i, c in ipairs(in_char) do 70 s = s:gsub(c, '\\' .. out_char[i]) 71 end 72 return s 73end 74 75-- Returns pos, did_find; there are two cases: 76-- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. 77-- 2. Delimiter not found: pos = pos after leading space; did_find = false. 78-- This throws an error if err_if_missing is true and the delim is not found. 79local function skip_delim(str, pos, delim, err_if_missing) 80 pos = pos + #str:match('^%s*', pos) 81 if str:sub(pos, pos) ~= delim then 82 if err_if_missing then 83 error('Expected ' .. delim .. ' near position ' .. pos) 84 end 85 return pos, false 86 end 87 return pos + 1, true 88end 89 90-- Expects the given pos to be the first character after the opening quote. 91-- Returns val, pos; the returned pos is after the closing quote character. 92local function parse_str_val(str, pos, val) 93 val = val or '' 94 local early_end_error = 'End of input found while parsing string.' 95 if pos > #str then error(early_end_error) end 96 local c = str:sub(pos, pos) 97 if c == '"' then return val, pos + 1 end 98 if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end 99 -- We must have a \ character. 100 local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} 101 local nextc = str:sub(pos + 1, pos + 1) 102 if not nextc then error(early_end_error) end 103 return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) 104end 105 106-- Returns val, pos; the returned pos is after the number's final character. 107local function parse_num_val(str, pos) 108 local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) 109 local val = tonumber(num_str) 110 if not val then error('Error parsing number at position ' .. pos .. '.') end 111 return val, pos + #num_str 112end 113 114 115-- Public values and functions. 116 117function json.stringify(obj, as_key) 118 local s = {} -- We'll build the string as an array of strings to be concatenated. 119 local kind = kind_of(obj) -- This is 'array' if it's an array or type(obj) otherwise. 120 if kind == 'array' then 121 if as_key then error('Can\'t encode array as key.') end 122 s[#s + 1] = '[' 123 for i, val in ipairs(obj) do 124 if i > 1 then s[#s + 1] = ', ' end 125 s[#s + 1] = json.stringify(val) 126 end 127 s[#s + 1] = ']' 128 elseif kind == 'table' then 129 if as_key then error('Can\'t encode table as key.') end 130 s[#s + 1] = '{' 131 for k, v in pairs(obj) do 132 if #s > 1 then s[#s + 1] = ', ' end 133 s[#s + 1] = json.stringify(k, true) 134 s[#s + 1] = ':' 135 s[#s + 1] = json.stringify(v) 136 end 137 s[#s + 1] = '}' 138 elseif kind == 'string' then 139 return '"' .. escape_str(obj) .. '"' 140 elseif kind == 'number' then 141 if as_key then return '"' .. tostring(obj) .. '"' end 142 return tostring(obj) 143 elseif kind == 'boolean' then 144 return tostring(obj) 145 elseif kind == 'nil' then 146 return 'null' 147 else 148 error('Unjsonifiable type: ' .. kind .. '.') 149 end 150 return table.concat(s) 151end 152 153json.null = {} -- This is a one-off table to represent the null value. 154 155function json.parse(str, pos, end_delim) 156 pos = pos or 1 157 if pos > #str then error('Reached unexpected end of input.') end 158 local pos = pos + #str:match('^%s*', pos) -- Skip whitespace. 159 local first = str:sub(pos, pos) 160 if first == '{' then -- Parse an object. 161 local obj, key, delim_found = {}, true, true 162 pos = pos + 1 163 while true do 164 key, pos = json.parse(str, pos, '}') 165 if key == nil then return obj, pos end 166 if not delim_found then error('Comma missing between object items.') end 167 pos = skip_delim(str, pos, ':', true) -- true -> error if missing. 168 obj[key], pos = json.parse(str, pos) 169 pos, delim_found = skip_delim(str, pos, ',') 170 end 171 elseif first == '[' then -- Parse an array. 172 local arr, val, delim_found = {}, true, true 173 pos = pos + 1 174 while true do 175 val, pos = json.parse(str, pos, ']') 176 if val == nil then return arr, pos end 177 if not delim_found then error('Comma missing between array items.') end 178 arr[#arr + 1] = val 179 pos, delim_found = skip_delim(str, pos, ',') 180 end 181 elseif first == '"' then -- Parse a string. 182 return parse_str_val(str, pos + 1) 183 elseif first == '-' or first:match('%d') then -- Parse a number. 184 return parse_num_val(str, pos) 185 elseif first == end_delim then -- End of an object or array. 186 return nil, pos + 1 187 else -- Parse true, false, or null. 188 local literals = {['true'] = true, ['false'] = false, ['null'] = json.null} 189 for lit_str, lit_val in pairs(literals) do 190 local lit_end = pos + #lit_str - 1 191 if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end 192 end 193 local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) 194 error('Invalid json syntax starting at ' .. pos_info_str) 195 end 196end 197 198return json 199