• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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