Parsing and Generating JSON in Lua

· 7 min read · Updated April 1, 2026 · intermediate
json serialization stdlib data-exchange

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:

  1. cjson.null is userdata, not nil. When you decode {"key": null}, the value is cjson.null (a userdata object). This means tbl.key == nil is false even though the JSON value was null. Use if tbl.key == cjson.null then to detect it.

  2. Encoding cjson.null raises an error. Where dkjson accepts json.null, cjson does not. Use nil when encoding, and cjson converts it to null automatically. If you pass cjson.null to encode(), 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.