一、前言:为什么不能直接用 cjson.encode/decode?
在 OpenResty 中处理 JSON 时,很多开发者直接这样写:
Lua
local cjson = require "cjson"
local data = cjson.decode(ngx.var.body) -- 危险!
但这样做存在严重隐患:
- ❌ 解析失败导致 worker 崩溃(如传入非法 JSON)
- ❌ 中文乱码(未指定编码)
- ❌ 空值处理不一致 (
null→nil导致字段丢失) - ❌ 循环引用死循环
解决方案:封装一个安全、健壮、易用的 CJSON 工具类!
本文将带你实现一个生产级 JSON 工具模块,彻底解决上述问题。
二、核心设计原则
我们的 json_util.lua 应具备:
✅ 使用 cjson.safe 避免崩溃
✅ 统一错误返回格式
✅ 支持空值保留(null → cjson.null)
✅ 自动处理中文编码
✅ 提供快捷方法(如 parse, stringify)
三、完整工具类实现
创建文件:/usr/local/openresty/lualib/lib/json_util.lua
Lua
-- lib/json_util.lua
local cjson_safe = require "cjson.safe"
-- 配置全局行为
cjson_safe.encode_empty_table_as_object(false) -- {} → []
cjson_safe.encode_sparse_array(true, 100, 10) -- 容忍稀疏数组
local _M = {}
--- 安全解析 JSON 字符串
-- @param str string: JSON 字符串
-- @return table|nil, string|nil: 解析结果, 错误信息
function _M.parse(str)
if type(str) ~= "string" or str == "" then
return nil, "input is not a valid string"
end
local data, err = cjson_safe.decode(str)
if not data then
return nil, "JSON decode failed: " .. tostring(err)
end
return data, nil
end
--- 安全序列化 Lua 对象为 JSON
-- @param obj any: Lua 对象(table, number, string...)
-- @return string|nil, string|nil: JSON 字符串, 错误信息
function _M.stringify(obj)
local json_str, err = cjson_safe.encode(obj)
if not json_str then
return nil, "JSON encode failed: " .. tostring(err)
end
return json_str, nil
end
--- 深度复制并过滤不可序列化字段(可选)
function _M.safe_stringify(obj)
local function safe_copy(t)
if type(t) ~= "table" then return t end
local new_t = {}
for k, v in pairs(t) do
if type(v) == "table" then
new_t[k] = safe_copy(v)
elseif type(v) == "function" or type(v) == "thread" or type(v) == "userdata" then
-- 跳过不可序列化类型
else
new_t[k] = v
end
end
return new_t
end
return _M.stringify(safe_copy(obj))
end
--- 快捷方法:解析后自动填充默认值
function _M.parse_with_defaults(str, defaults)
local data, err = _M.parse(str)
if err then return nil, err end
if type(defaults) == "table" then
for k, v in pairs(defaults) do
if data[k] == nil then
data[k] = v
end
end
end
return data, nil
end
return _M
四、使用示例
4.1 安全解析请求体(推荐!)
Lua
location /api {
content_by_lua_block {
local json_util = require "lib.json_util"
ngx.req.read_body()
local body = ngx.req.get_body_data()
local req_data, err = json_util.parse(body)
if err then
ngx.status = 400
ngx.say('{"error":"', err, '"}')
return
end
-- 处理业务逻辑
ngx.say("User ID: ", req_data.userId or "unknown")
}
}
4.2 返回结构化 JSON 响应
Lua
local response = {
code = 200,
data = { name = "Alice", age = 30 },
timestamp = ngx.now()
}
local json_str, err = json_util.stringify(response)
if err then
ngx.log(ngx.ERR, "Response serialize failed: ", err)
ngx.exit(500)
end
ngx.say(json_str)
4.3 带默认值的解析(防字段缺失)
Lua
local defaults = { page = 1, size = 10 }
local params, err = json_util.parse_with_defaults(body, defaults)
-- 若 body 中无 page,则 params.page = 1
五、关键特性详解
✅ 1. 使用 cjson.safe 防崩溃
- 原生
cjson.decode在非法 JSON 时会抛出 Lua error,导致 worker 退出 cjson.safe返回nil + error,可安全捕获
✅ 2. 空值(null)处理
- 默认情况下,JSON 的
null会被转为 Lua 的nil,导致字段消失 - 如需保留
null,可在 decode 后处理,或使用cjson.null标记
✅ 3. 中文支持
- OpenResty 默认使用 UTF-8,只要客户端发送 UTF-8 JSON,无需额外处理
- 确保
Content-Type: application/json; charset=utf-8
✅ 4. 循环引用防护
cjson.safe内置深度限制(默认 1000 层),避免死循环
六、生产环境配置建议
在 nginx.conf 中设置合理的 JSON 限制:
Lua
http {
# 限制请求体大小(防 OOM)
client_max_body_size 1m;
# Lua 共享字典(可选:缓存常用 JSON 模板)
lua_shared_dict json_cache 10m;
}
七、常见错误与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
attempt to index a nil value |
JSON 字段不存在且未判空 | 使用 req_data.field or default |
| 中文乱码 | 客户端未用 UTF-8 编码 | 要求客户端设置正确 charset |
| 大数字精度丢失 | Lua number 是 double | 启用 cjson.decode_big_numbers_as_strings(true) |
| 数组变对象 | 空 table {} 被序列化为 {} |
调用 encode_empty_table_as_object(false) |
💡 大数字处理示例:
Luacjson_safe.decode_big_numbers_as_strings(true) local data = cjson_safe.decode('{"id": 12345678901234567890}') -- data.id = "12345678901234567890" (string)
八、性能提示
- ✅
cjson是 C 实现,比纯 Lua JSON 库快 10~50 倍 - ✅ 单次 encode/decode 耗时通常 < 0.1ms
- ✅ 避免在循环中反复序列化大对象
九、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!