All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
2 local always_try_using_lpeg = true
3 local register_global_module_table = false
4 local global_module_name = 'json'
8 David Kolf's JSON module for Lua 5.1/5.2
13 For the documentation see the corresponding readme.txt or visit
14 <http://dkolf.de/src/dkjson-lua.fsl/>.
16 You can contact the author by sending an e-mail to 'david' at the
20 Copyright (C) 2010-2013 David Heiko Kolf
22 Permission is hereby granted, free of charge, to any person obtaining
23 a copy of this software and associated documentation files (the
24 "Software"), to deal in the Software without restriction, including
25 without limitation the rights to use, copy, modify, merge, publish,
26 distribute, sublicense, and/or sell copies of the Software, and to
27 permit persons to whom the Software is furnished to do so, subject to
28 the following conditions:
30 The above copyright notice and this permission notice shall be
31 included in all copies or substantial portions of the Software.
33 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
34 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
35 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
36 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
37 BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
38 ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
39 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
44 -- global dependencies:
45 local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset =
46 pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset
47 local error, require, pcall, select = error, require, pcall, select
48 local floor, huge = math.floor, math.huge
49 local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =
50 string.rep, string.gsub, string.sub, string.byte, string.char,
51 string.find, string.len, string.format
52 local strmatch = string.match
53 local concat = table.concat
55 local json = { version = "dkjson 2.5" }
57 if register_global_module_table then
58 _G[global_module_name] = json
61 local _ENV = nil -- blocking globals in Lua 5.2
64 -- Enable access to blocked metatables.
65 -- Don't worry, this module doesn't change anything in them.
66 local debmeta = require "debug".getmetatable
67 if debmeta then getmetatable = debmeta end
70 json.null = setmetatable ({}, {
71 __tojson = function () return "null" end
74 local function isarray (tbl)
75 local max, n, arraylen = 0, 0, 0
76 for k,v in pairs (tbl) do
77 if k == 'n' and type(v) == 'number' then
83 if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
92 if max > 10 and max > arraylen and max > n * 2 then
93 return false -- don't create an array with too many holes
99 ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
100 ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"
103 local function escapeutf8 (uchar)
104 local value = escapecodes[uchar]
108 local a, b, c, d = strbyte (uchar, 1, 4)
109 a, b, c, d = a or 0, b or 0, c or 0, d or 0
112 elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
113 value = (a - 0xc0) * 0x40 + b - 0x80
114 elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
115 value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
116 elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
117 value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
121 if value <= 0xffff then
122 return strformat ("\\u%.4x", value)
123 elseif value <= 0x10ffff then
124 -- encode as UTF-16 surrogate pair
125 value = value - 0x10000
126 local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)
127 return strformat ("\\u%.4x\\u%.4x", highsur, lowsur)
133 local function fsub (str, pattern, repl)
134 -- gsub always builds a new string in a buffer, even when no match
135 -- exists. First using find should be more efficient when most strings
136 -- don't contain the pattern.
137 if strfind (str, pattern) then
138 return gsub (str, pattern, repl)
144 local function quotestring (value)
145 -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
146 value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8)
147 if strfind (value, "[\194\216\220\225\226\239]") then
148 value = fsub (value, "\194[\128-\159\173]", escapeutf8)
149 value = fsub (value, "\216[\128-\132]", escapeutf8)
150 value = fsub (value, "\220\143", escapeutf8)
151 value = fsub (value, "\225\158[\180\181]", escapeutf8)
152 value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8)
153 value = fsub (value, "\226\129[\160-\175]", escapeutf8)
154 value = fsub (value, "\239\187\191", escapeutf8)
155 value = fsub (value, "\239\191[\176-\191]", escapeutf8)
157 return "\"" .. value .. "\""
159 json.quotestring = quotestring
161 local function replace(str, o, n)
162 local i, j = strfind (str, o, 1, true)
164 return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1)
170 -- locale independent num2str and str2num functions
171 local decpoint, numfilter
173 local function updatedecpoint ()
174 decpoint = strmatch(tostring(0.5), "([^05+])")
175 -- build a filter that can be used to remove group separators
176 numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+"
181 local function num2str (num)
182 return replace(fsub(tostring(num), numfilter, ""), decpoint, ".")
185 local function str2num (str)
186 local num = tonumber(replace(str, ".", decpoint))
189 num = tonumber(replace(str, ".", decpoint))
194 local function addnewline2 (level, buffer, buflen)
195 buffer[buflen+1] = "\n"
196 buffer[buflen+2] = strrep (" ", level)
201 function json.addnewline (state)
203 state.bufferlen = addnewline2 (state.level or 0,
204 state.buffer, state.bufferlen or #(state.buffer))
208 local encode2 -- forward declaration
210 local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state)
211 local kt = type (key)
212 if kt ~= 'string' and kt ~= 'number' then
213 return nil, "type '" .. kt .. "' is not supported as a key by JSON."
220 buflen = addnewline2 (level, buffer, buflen)
222 buffer[buflen+1] = quotestring (key)
223 buffer[buflen+2] = ":"
224 return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state)
227 local function appendcustom(res, buffer, state)
228 local buflen = state.bufferlen
229 if type (res) == 'string' then
236 local function exception(reason, value, state, buffer, buflen, defaultmessage)
237 defaultmessage = defaultmessage or reason
238 local handler = state.exception
240 return nil, defaultmessage
242 state.bufferlen = buflen
243 local ret, msg = handler (reason, value, state, defaultmessage)
244 if not ret then return nil, msg or defaultmessage end
245 return appendcustom(ret, buffer, state)
249 function json.encodeexception(reason, value, state, defaultmessage)
250 return quotestring("<" .. defaultmessage .. ">")
253 encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state)
254 local valtype = type (value)
255 local valmeta = getmetatable (value)
256 valmeta = type (valmeta) == 'table' and valmeta -- only tables
257 local valtojson = valmeta and valmeta.__tojson
259 if tables[value] then
260 return exception('reference cycle', value, state, buffer, buflen)
263 state.bufferlen = buflen
264 local ret, msg = valtojson (value, state)
265 if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end
267 buflen = appendcustom(ret, buffer, state)
268 elseif value == nil then
270 buffer[buflen] = "null"
271 elseif valtype == 'number' then
273 if value ~= value or value >= huge or -value >= huge then
274 -- This is the behaviour of the original JSON implementation.
281 elseif valtype == 'boolean' then
283 buffer[buflen] = value and "true" or "false"
284 elseif valtype == 'string' then
286 buffer[buflen] = quotestring (value)
287 elseif valtype == 'table' then
288 if tables[value] then
289 return exception('reference cycle', value, state, buffer, buflen)
293 local isa, n = isarray (value)
294 if n == 0 and valmeta and valmeta.__jsontype == 'object' then
298 if isa then -- JSON array
302 buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state)
303 if not buflen then return nil, msg end
315 local order = valmeta and valmeta.__jsonorder or globalorder
324 buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
325 prev = true -- add a seperator before the next element
328 for k,v in pairs (value) do
330 buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
331 if not buflen then return nil, msg end
332 prev = true -- add a seperator before the next element
336 for k,v in pairs (value) do
337 buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
338 if not buflen then return nil, msg end
339 prev = true -- add a seperator before the next element
343 buflen = addnewline2 (level - 1, buffer, buflen)
350 return exception ('unsupported type', value, state, buffer, buflen,
351 "type '" .. valtype .. "' is not supported by JSON.")
356 function json.encode (value, state)
358 local oldbuffer = state.buffer
359 local buffer = oldbuffer or {}
360 state.buffer = buffer
362 local ret, msg = encode2 (value, state.indent, state.level or 0,
363 buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state)
366 elseif oldbuffer == buffer then
367 state.bufferlen = ret
370 state.bufferlen = nil
372 return concat (buffer)
376 local function loc (str, where)
377 local line, pos, linepos = 1, 1, 0
379 pos = strfind (str, "\n", pos, true)
380 if pos and pos < where then
388 return "line " .. line .. ", column " .. (where - linepos)
391 local function unterminated (str, what, where)
392 return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where)
395 local function scanwhite (str, pos)
397 pos = strfind (str, "%S", pos)
398 if not pos then return nil end
399 local sub2 = strsub (str, pos, pos + 1)
400 if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then
401 -- UTF-8 Byte Order Mark
403 elseif sub2 == "//" then
404 pos = strfind (str, "[\n\r]", pos + 2)
405 if not pos then return nil end
406 elseif sub2 == "/*" then
407 pos = strfind (str, "*/", pos + 2)
408 if not pos then return nil end
416 local escapechars = {
417 ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f",
418 ["n"] = "\n", ["r"] = "\r", ["t"] = "\t"
421 local function unichar (value)
424 elseif value <= 0x007f then
425 return strchar (value)
426 elseif value <= 0x07ff then
427 return strchar (0xc0 + floor(value/0x40),
428 0x80 + (floor(value) % 0x40))
429 elseif value <= 0xffff then
430 return strchar (0xe0 + floor(value/0x1000),
431 0x80 + (floor(value/0x40) % 0x40),
432 0x80 + (floor(value) % 0x40))
433 elseif value <= 0x10ffff then
434 return strchar (0xf0 + floor(value/0x40000),
435 0x80 + (floor(value/0x1000) % 0x40),
436 0x80 + (floor(value/0x40) % 0x40),
437 0x80 + (floor(value) % 0x40))
443 local function scanstring (str, pos)
444 local lastpos = pos + 1
445 local buffer, n = {}, 0
447 local nextpos = strfind (str, "[\"\\]", lastpos)
449 return unterminated (str, "string", pos)
451 if nextpos > lastpos then
453 buffer[n] = strsub (str, lastpos, nextpos - 1)
455 if strsub (str, nextpos, nextpos) == "\"" then
456 lastpos = nextpos + 1
459 local escchar = strsub (str, nextpos + 1, nextpos + 1)
461 if escchar == "u" then
462 value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16)
465 if 0xD800 <= value and value <= 0xDBff then
466 -- we have the high surrogate of UTF-16. Check if there is a
467 -- low surrogate escaped nearby to combine them.
468 if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then
469 value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16)
470 if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then
471 value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000
473 value2 = nil -- in case it was out of range for a low surrogate
477 value = value and unichar (value)
480 lastpos = nextpos + 12
482 lastpos = nextpos + 6
488 value = escapechars[escchar] or escchar
489 lastpos = nextpos + 2
496 return buffer[1], lastpos
498 return concat (buffer), lastpos
504 local scanvalue -- forward declaration
506 local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta)
507 local len = strlen (str)
509 local pos = startpos + 1
510 if what == 'object' then
511 setmetatable (tbl, objectmeta)
513 setmetatable (tbl, arraymeta)
516 pos = scanwhite (str, pos)
517 if not pos then return unterminated (str, what, startpos) end
518 local char = strsub (str, pos, pos)
519 if char == closechar then
523 val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
524 if err then return nil, pos, err end
525 pos = scanwhite (str, pos)
526 if not pos then return unterminated (str, what, startpos) end
527 char = strsub (str, pos, pos)
530 return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")"
532 pos = scanwhite (str, pos + 1)
533 if not pos then return unterminated (str, what, startpos) end
535 val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
536 if err then return nil, pos, err end
538 pos = scanwhite (str, pos)
539 if not pos then return unterminated (str, what, startpos) end
540 char = strsub (str, pos, pos)
551 scanvalue = function (str, pos, nullval, objectmeta, arraymeta)
553 pos = scanwhite (str, pos)
555 return nil, strlen (str) + 1, "no valid JSON value (reached the end)"
557 local char = strsub (str, pos, pos)
559 return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta)
560 elseif char == "[" then
561 return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta)
562 elseif char == "\"" then
563 return scanstring (str, pos)
565 local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos)
567 local number = str2num (strsub (str, pstart, pend))
569 return number, pend + 1
572 pstart, pend = strfind (str, "^%a%w*", pos)
574 local name = strsub (str, pstart, pend)
575 if name == "true" then
576 return true, pend + 1
577 elseif name == "false" then
578 return false, pend + 1
579 elseif name == "null" then
580 return nullval, pend + 1
583 return nil, pos, "no valid JSON value at " .. loc (str, pos)
587 local function optionalmetatables(...)
588 if select("#", ...) > 0 then
591 return {__jsontype = 'object'}, {__jsontype = 'array'}
595 function json.decode (str, pos, nullval, ...)
596 local objectmeta, arraymeta = optionalmetatables(...)
597 return scanvalue (str, pos, nullval, objectmeta, arraymeta)
600 function json.use_lpeg ()
601 local g = require ("lpeg")
603 if g.version() == "0.11" then
604 error "due to a bug in LPeg 0.11, it cannot be used for JSON matching"
607 local pegmatch = g.match
608 local P, S, R = g.P, g.S, g.R
610 local function ErrorCall (str, pos, msg, state)
611 if not state.msg then
612 state.msg = msg .. " at " .. loc (str, pos)
618 local function Err (msg)
619 return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall)
622 local SingleLineComment = P"//" * (1 - S"\n\r")^0
623 local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/"
624 local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0
626 local PlainChar = 1 - S"\"\\\n\r"
627 local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars
628 local HexDigit = R("09", "af", "AF")
629 local function UTF16Surrogate (match, pos, high, low)
630 high, low = tonumber (high, 16), tonumber (low, 16)
631 if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then
632 return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)
637 local function UTF16BMP (hex)
638 return unichar (tonumber (hex, 16))
640 local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit))
641 local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP
642 local Char = UnicodeEscape + EscapeSequence + PlainChar
643 local String = P"\"" * g.Cs (Char ^ 0) * (P"\"" + Err "unterminated string")
644 local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0))
645 local Fractal = P"." * R"09"^0
646 local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1
647 local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num
648 local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1)
649 local SimpleValue = Number + String + Constant
650 local ArrayContent, ObjectContent
652 -- The functions parsearray and parseobject parse only a single value/pair
653 -- at a time and store them directly to avoid hitting the LPeg limits.
654 local function parsearray (str, pos, nullval, state)
659 obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state)
660 if not npos then break end
665 return pos, setmetatable (t, state.arraymeta)
668 local function parseobject (str, pos, nullval, state)
673 key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state)
674 if not npos then break end
678 return pos, setmetatable (t, state.objectmeta)
681 local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P"]" + Err "']' expected")
682 local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P"}" + Err "'}' expected")
683 local Value = Space * (Array + Object + SimpleValue)
684 local ExpectedValue = Value + Space * Err "value expected"
685 ArrayContent = Value * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
686 local Pair = g.Cg (Space * String * Space * (P":" + Err "colon expected") * ExpectedValue)
687 ObjectContent = Pair * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
688 local DecodeValue = ExpectedValue * g.Cp ()
690 function json.decode (str, pos, nullval, ...)
692 state.objectmeta, state.arraymeta = optionalmetatables(...)
693 local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state)
695 return nil, state.pos, state.msg
701 -- use this function only once:
702 json.use_lpeg = function () return json end
704 json.using_lpeg = true
706 return json -- so you can get the module using json = require "dkjson".use_lpeg()
709 if always_try_using_lpeg then
710 pcall (json.use_lpeg)