Parsing and Generating JSON in Lua
JSON is the lingua franca of data exchange. Whether you’re reading a config file, consuming an API response, or logging structured data, you will encounter JSON in almost every Lua project. The catch: Lua ships with no built-in JSON support. You need a library.
This guide focuses on dkjson, the most widely-used pure-Lua JSON library. It is dependency-free, works across Lua 5.1 through 5.4 and LuaJIT, and can be installed in seconds or simply copied into your project. I’ll also show you the alternatives and the key differences that matter.
Installing a JSON Library
Via LuaRocks
The fastest route on any system with LuaRocks installed:
luarocks install dkjson
For the C-based alternative:
luarocks install lua-cjson
Manual Install
dkjson is a single .lua file with no dependencies. Download it from David Kolf’s GitHub repository and drop it anywhere in your package.path, or alongside your script. This makes it ideal for projects where you cannot or do not want to manage external dependencies.
Once installed, load it the same way as any other module:
local json = require("dkjson")
Encoding Lua Tables to JSON
JSON encoding converts a Lua table into a JSON string. With dkjson, you pass a table to json.encode():
local json = require("dkjson")
local data = {
name = "Grace Hopper",
born = 1906,
active = true,
languages = { "COBOL", "FLOW-MATIC", "ALGOL" }
}
local str = json.encode(data)
print(str)
Output:
{"name":"Grace Hopper","born":1906,"active":true,"languages":["COBOL","FLOW-MATIC","ALGOL"]}
Pretty-Printing
By default, the output is compact. Pass { indent = true } as the second argument to get formatted output:
local str = json.encode(data, { indent = true })
print(str)
{
"name": "Grace Hopper",
"born": 1906,
"active": true,
"languages": ["COBOL", "FLOW-MATIC", "ALGOL"]
}
Controlling Key Order
JSON objects have no defined key order, and by default dkjson outputs keys in whatever order Lua’s table iteration produces. Use keyorder to enforce a predictable order:
local str = json.encode(data, {
keyorder = { "name", "born", "active", "languages" },
indent = true
})
Representing Null Values
This is where Lua trips up many newcomers. Lua tables cannot contain nil as a value — setting a key to nil deletes the key entirely. So if you want to represent JSON null, you need dkjson’s sentinel value json.null:
local record = {
title = "Draft Report",
content = "Some text here",
reviewed_by = json.null -- this will become null in JSON
}
local str = json.encode(record, { indent = true })
print(str)
{
"title": "Draft Report",
"content": "Some text here",
"reviewed_by": null
}
Without json.null, the reviewed_by key would simply disappear from the output.
Decoding JSON to Lua Tables
Parsing JSON with dkjson uses json.decode(). It takes a JSON string and returns a Lua table along with a byte position and an error message:
local str = '{"currency": "\u20AC", "amount": 42}'
local obj, pos, err = json.decode(str, 1, nil)
if err then
print("Parse error at position " .. pos .. ": " .. err)
else
print("Currency:", obj.currency) --> Currency: €
print("Amount:", obj.amount) --> Amount: 42
end
Important: dkjson decodes JSON null to Lua nil. This means you cannot distinguish between a missing key and an explicit null after decoding — both result in nil. If you need to tell them apart, pass a sentinel value as the third argument:
local obj = json.decode('{"active": null, "name": "Test"}', 1, "NULL_MARKER")
print(obj.active) --> NULL_MARKER
print(obj.name) --> Test
Error Handling
Always check the third return value when decoding. A malformed JSON string causes dkjson to return nil, nil, "error message":
local function safe_decode(str)
local obj, pos, err = json.decode(str, 1, nil)
if err then
return nil, "JSON parse error at position " .. pos .. ": " .. err
end
return obj
end
local ok, result = safe_decode('{"valid": true}')
print(ok, result.valid) --> true true
local ok2, err2 = safe_decode('{"broken": }')
print(ok2, err2) --> nil JSON parse error at position 12: <value> expected near '}'
Wrapping your decode calls this way lets you surface clear errors to the user or logs instead of silently failing.
Working with Nested Data
Real-world JSON is rarely flat. Here is a more complex example showing nested objects and arrays:
local config = {
server = {
host = "api.example.com",
port = 443,
tls = true
},
routes = {
{ method = "GET", path = "/health" },
{ method = "POST", path = "/events" },
{ method = "DELETE", path = "/events/{id}" }
},
limits = {
requests_per_second = 100,
burst = json.null -- no burst limit
}
}
local str = json.encode(config, { indent = true })
print(str)
{
"server": {
"host": "api.example.com",
"port": 443,
"tls": true
},
"routes": [
{ "method": "GET", "path": "/health" },
{ "method": "POST", "path": "/events" },
{ "method": "DELETE", "path": "/events/{id}" }
],
"limits": {
"requests_per_second": 100,
"burst": null
}
}
Lua arrays (sequences) map naturally to JSON arrays, and Lua tables with non-sequential keys map to JSON objects.
Alternatives to dkjson
lua-cjson
The C extension lua-cjson is significantly faster than dkjson for large documents because it runs in C rather than interpreted Lua. Install it with luarocks install lua-cjson.
local cjson = require("cjson")
local str = cjson.encode({ key = "value" })
local tbl = cjson.decode('{"key": "value"}')
The API is simpler, but there are two critical differences:
-
cjson.nullis userdata, notnil. When you decode{"key": null}, the value iscjson.null(a userdata object). This meanstbl.key == nilisfalseeven though the JSON value was null. Useif tbl.key == cjson.null thento detect it. -
Encoding
cjson.nullraises an error. Where dkjson acceptsjson.null, cjson does not. Usenilwhen encoding, and cjson converts it tonullautomatically. If you passcjson.nulltoencode(), you get an error.
For high-throughput services processing megabytes of JSON per second, cjson’s speed is worth the quirks. For everything else, dkjson is the simpler choice.
json.lua
Pure Lua, faster than dkjson, and created by Ralph G.wei. Install it with luarocks install json-lua.
local json = require("json")
json.encode({ key = "value" })
json.decode('{"key": "value"}')
The API matches dkjson closely, but json.lua maps JSON null directly to Lua nil. Like dkjson, this means you cannot distinguish between a missing key and an explicit null after decoding.
Common Gotchas
nil Values Disappear
Lua tables cannot hold nil. Any key set to nil is deleted. json.encode() silently omits these keys:
local data = { a = 1, b = nil, c = 3 }
print(json.encode(data)) --> {"a":1,"c":3}
If null semantics matter, use json.null with dkjson or cjson.null with cjson.
Array Indices Start at 1
JSON arrays are conceptually zero-indexed, but when dkjson decodes [1, 2, 3], you get a Lua array where t[1] == 1. This is almost always what you want — just be aware when interfacing with systems that expect zero-based indexing.
Large Integers Lose Precision
Lua numbers are IEEE 754 doubles. Integers larger than 2^53 (about 9 quadrillion) cannot be represented exactly. If you are working with financial data or IDs from systems that use 64-bit integers, encode them as strings instead of numbers to preserve precision.
Circular References Crash the Encoder
JSON has no way to represent circular references. If your table has a self-referential structure, json.encode() will loop forever or raise an error. Validate your data structure before encoding:
local function is_cyclic(tbl)
local seen = {}
local function walk(t)
if type(t) ~= "table" then return false end
if seen[t] then return true end
seen[t] = true
for _, v in pairs(t) do
if walk(v) then return true end
end
return false
end
return walk(tbl)
end
UTF-8 Encoding Required
dkjson expects UTF-8 input. If your Lua environment or source files use a different encoding, Unicode characters may be corrupted or raise errors. Stick to UTF-8 and you will not have problems.
Where to Go Next
If you found this useful, you might also explore Lua’s table serialization patterns (tables are the backbone of all data handling in Lua), and how Lua’s module system (require) loads libraries like dkjson. These topics come up frequently whenever you move beyond simple JSON tasks into more complex data pipelines.
For most JSON tasks in production Lua code, dkjson handles the job without fuss. Keep json.null in mind whenever you need explicit null semantics, and always validate your data structure before passing it to the encoder.