mc::json 模块
概述
mc::json 提供 mc::json_value/mc::json_object/mc::json_array 与 JSON 文本之间的编解码能力,支持格式化输出、浮点精度控制、按字段精确格式化(JSONPath 子集)、循环引用检测和解码资源限制。这三个名称分别是 mc::variant、mc::dict 和 mc::variant::array_type 的语义别名,不引入新的运行时类型。
主要特性
- JSON 语义别名:使用
json_value/json_object/json_array表达接口意图,同时保持与variant/dict/variants完全相同的类型和 ABI - variant 原生编解码:
json_encode/json_decode直接复用 variant 及其引用语义容器,不做数据深拷贝 - 循环引用检测:编码时精确识别 dict/数组的自引用与间接循环,异常携带出错字段的 JSONPath;DAG 共享(同一子对象被多处引用但无环)正常展开
- 按字段浮点格式化:
field_format_rules用 RFC 9535 JSONPath 子集定位字段,为不同字段配置不同的精度与记法;规则编译为共享前缀树,编码单趟匹配,开销与规则条数无关 - 覆盖审计:
unmatched_float_policy的 warn/strict 策略可机器检查"漏配"的浮点字段 - 安全限制:解码支持最大深度、输入长度、字符串长度、数组/对象大小限制,防御恶意输入
基本用法
编解码与全局浮点精度
#include <mc/dict.h>
#include <mc/json.h>
#include <mc/variant.h>
using namespace mc;
using namespace mc::json;
// 编码
json_object obj{{"name", "sensor0"}, {"reading", 3.14159}};
mc::string text = json_encode(obj); // {"name":"sensor0","reading":3.1415899999999999}
// 全局浮点精度:2 位小数定宽
json_encode_options options;
options.float_precision = 2;
options.notation = float_notation::fixed;
mc::string fixed = json_encode(obj, options); // {"name":"sensor0","reading":3.14}
// 解码
json_value decoded = json_decode(text);
EXPECT_TRUE(decoded["reading"].is_double());
// 显式 JSON null;json_value{} 仍是普通 nil
json_value null_value = json_value::make_json_null();
EXPECT_EQ(json_encode(null_value), "null");
按字段精确格式化(JSONPath 子集)
#include <mc/json.h>
using namespace mc::json;
json_encode_options options;
// 规则在 add 时编译,options 可跨多次 json_encode 复用
options.field_formats.add("$.Sensors[*].Reading", {2, float_notation::fixed})
.add("$.Sensors[*].Thresholds.*", {1, float_notation::fixed})
.add("$..ApparentVA", {1, float_notation::fixed});
// 配置也可以来自 JSON 文件(值为整数时是 precision 的简写)
options.field_formats = field_format_rules::from_json(R"({
"$.Sensors[*].Reading": {"precision": 2, "notation": "fixed"},
"$.PowerWatts": 1
})");
mc::dict payload{{"Sensors", mc::variants{mc::dict{{"Reading", 25.7361}}}}, {"PowerWatts", 133.72}};
mc::string text = json_encode(payload, options);
// {"Sensors":[{"Reading":25.74}],"PowerWatts":1e+02}
覆盖审计与循环引用
#include <mc/json.h>
using namespace mc::json;
// strict:开发/CI 阶段发现漏配字段,异常携带字段 JSONPath
json_encode_options options;
options.field_formats.add("$..Reading", {2, float_notation::fixed});
options.unmatched_floats = unmatched_float_policy::strict;
try {
json_encode(payload, options);
} catch (const mc::parse_error_exception& e) {
// "Float field not covered by field_formats at $.PowerWatts"
}
// 循环引用:异常携带循环发生位置
mc::dict d;
d("self", d);
try {
json_encode(d);
} catch (const mc::parse_error_exception& e) {
// "Circular reference detected at $.self"
}
API 详细说明
JSON 类型别名
using json_value = variant;
using json_object = json_value::object_type;
using json_array = json_value::array_type;
| 别名 | 实际类型 | 说明 |
|---|---|---|
mc::json_value |
mc::variant |
任意 JSON 值;调用 make_json_null() 构造显式 JSON null |
mc::json_object |
mc::dict |
JSON object,保持 dict 的有序键语义和引用语义 |
mc::json_array |
mc::variant::array_type(当前为 mc::variants) |
JSON array,类型始终与 variant 配置采用的数组类型一致 |
别名不改变类型布局、符号签名或拷贝语义。普通 json_value{} 与 nullptr 表示通用 nil;json_value::make_json_null() 表示带 JSON 来源语义的 null。两者由 json_encode 直接编码时都输出 null,但只有后者在 Lua 等跨语言转换后仍保留显式 JSON null 语义。
枚举
enum class float_notation {
significant, // 有效数字(%g 风格):自动去尾零
fixed, // 固定小数位(%f 风格):不足补零
};
enum class unmatched_float_policy {
ignore, // 未覆盖浮点走全局 float_precision/notation(默认)
warn, // 同 ignore,编码完成后以 wlog 输出未覆盖字段的 JSONPath 清单
strict, // 遇到未覆盖浮点立即抛异常,异常携带字段 JSONPath
};
struct field_format
单个字段的浮点格式化规则。
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
float_precision |
int |
-1 |
精度,语义同 json_encode_options::float_precision,范围 [-1, 17] |
notation |
float_notation |
significant |
记法,语义同 json_encode_options::notation |
class field_format_rules
按字段浮点格式化规则集。规则在添加时编译为共享前缀树(公共前缀只存一份),编码时随文档单趟遍历推进状态集;同一对象可跨多次 json_encode 复用,路径解析只发生一次。拷贝为浅拷贝,共享编译结果。
| 方法 | 参数 | 返回值 | 异常 | 线程安全 | 说明 |
|---|---|---|---|---|---|
add(path, format) |
mc::string_view 路径, const field_format& |
field_format_rules& |
路径语法非法抛 mc::parse_error_exception |
否(构建期单线程) | 添加一条规则,支持链式调用 |
from_dict(config) |
const json_object& |
field_format_rules |
配置格式或路径非法抛 mc::parse_error_exception |
是(静态函数) | 键为 JSONPath,值为 {"precision": int, "notation": "fixed"/"significant"} 或整数简写(即 precision) |
from_json(json_config) |
mc::string_view |
field_format_rules |
文本不是合法 JSON 对象或路径非法抛 mc::parse_error_exception |
是(静态函数) | 从 JSON 文本构建,格式同 from_dict |
empty() |
无 | bool |
不抛出 | 是(只读) | 是否没有任何规则 |
size() |
无 | size_t |
不抛出 | 是(只读) | 规则条数 |
trie() |
无 | const detail::format_trie* |
不抛出 | 是(只读) | 编译后的前缀树,编码器内部使用 |
路径语法(RFC 9535 JSONPath 子集):
| 语法 | 含义 |
|---|---|
$ |
根节点,每条路径必须以此开头 |
.name |
子成员(shorthand 允许字母、数字、_、@、- 及非 ASCII 字符) |
['name'] |
子成员括号记法,成员名含特殊字符(如 .)时使用,支持 \' \" \\ 转义 |
[n] |
数组下标 |
[*] / .* |
通配一层(键或下标均可) |
..name |
递归下降:任意深度的后代(含直接子成员) |
不支持过滤表达式 ?()、切片和联合选择器。
匹配语义:规则匹配到容器(对象/数组)时对整个子树内的浮点数生效;同一字段被多条规则命中时,就近祖先的规则优先,同一位置后添加的规则优先。
struct json_encode_options
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pretty_print |
bool |
false |
是否格式化输出(缩进与换行) |
indent_size |
int |
4 |
缩进空格数,范围 [0, 8] |
escape_non_ascii |
bool |
false |
是否将非 ASCII 字符转义为 \uXXXX |
sort_keys |
bool |
false |
是否按字典序排序对象键 |
float_precision |
int |
-1 |
全局浮点精度;-1 表示 max_digits10 往返精度,范围 [-1, 17] |
notation |
float_notation |
significant |
全局浮点记法,仅 float_precision >= 0 时生效 |
max_depth |
int |
32 |
最大嵌套深度,范围 [1, 512] |
field_formats |
field_format_rules |
空 | 按字段浮点格式化规则;为空时不启用路径匹配,行为与性能和不配置时一致 |
unmatched_floats |
unmatched_float_policy |
ignore |
未被规则覆盖的浮点字段处理策略 |
方法 normalize():将各字段截断到有效范围,不抛出异常,线程安全(仅修改自身)。
struct json_decode_options
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
max_depth |
int |
32 |
最大嵌套深度 |
max_input_length |
size_t |
16MB |
输入 JSON 文本最大字节数 |
max_string_length |
size_t |
64KB |
单个字符串(含键名)最大长度 |
max_array_size |
size_t |
65536 |
单个数组最大元素数 |
max_object_size |
size_t |
65536 |
单个对象最大键值对数 |
方法 normalize():将各字段调整到有效范围(均 >= 1),不抛出异常,线程安全(仅修改自身)。
json_encode
mc::string json_encode(const json_value& value,
const json_encode_options& options = json_encode_options::default_encode_options);
mc::string json_encode(const json_object& obj,
const json_encode_options& options = json_encode_options::default_encode_options);
mc::string json_encode(const json_array& arr,
const json_encode_options& options = json_encode_options::default_encode_options);
mc::string json_encode(const std::vector<json_value>& arr,
const json_encode_options& options = json_encode_options::default_encode_options);
| 参数 | 说明 |
|---|---|
value / obj / arr |
要编码的 JSON 值 / object / value 数组 |
options |
编码选项,缺省使用全局默认配置 |
- 返回编码后的 JSON 字符串
- 异常:编码失败(含循环引用、深度超限、strict 策略未覆盖浮点、非有限浮点数)抛
mc::parse_error_exception - 线程安全:是(options 只读,规则前缀树编译后只读)
std::vector<json_value>重载保留既有 ABI;其他可转换元素的std::vector通过头文件模板兼容并转发到json_value
json_decode
json_value json_decode(mc::string_view json,
const json_decode_options& options = json_decode_options::default_decode_options);
| 参数 | 说明 |
|---|---|
json |
JSON 文本,接受 mc::string/mc::string_view/const char* |
options |
解码选项,缺省使用全局默认配置 |
- 返回解码后的
json_value(JSON object →json_object,array →json_array,number → int64/uint64/double) - 异常:语法错误或超出资源限制抛
mc::parse_error_exception - 线程安全:是
最佳实践
1. 规则集编译一次、跨调用复用
// ✅ 推荐:启动时构建 options,热路径直接复用(路径只解析一次)
static const json_encode_options k_encode_options = [] {
json_encode_options opts;
opts.field_formats = field_format_rules::from_json(load_config_text());
return opts;
}();
mc::string out = json_encode(payload, k_encode_options);
// ❌ 不推荐:每次编码重新解析配置,路径编译开销重复发生
mc::string encode_payload(const dict& payload) {
json_encode_options opts;
opts.field_formats = field_format_rules::from_json(load_config_text()); // 每次都编译
return json_encode(payload, opts);
}
2. 用递归下降减少规则条数,用 strict 模式保证覆盖
// ✅ 推荐:同名字段用 “..” 一条覆盖;开发/CI 阶段开 strict 及早暴露漏配
json_encode_options opts;
opts.field_formats.add("$..Reading", {2, float_notation::fixed});
#ifndef NDEBUG
opts.unmatched_floats = unmatched_float_policy::strict;
#endif
// ❌ 不推荐:逐字段罗列同名路径,新增字段时容易漏配且无人发现
opts.field_formats.add("$.Sensors[0].Reading", {2, float_notation::fixed})
.add("$.Sensors[1].Reading", {2, float_notation::fixed}); // Sensors[2] 出现时漏配
3. 循环引用是数据构造缺陷,按异常路径修数据
// ✅ 推荐:捕获异常并根据消息中的 JSONPath 定位数据构造代码
try {
return json_encode(payload);
} catch (const mc::parse_error_exception& e) {
elog("encode failed: ${err}", ("err", e.what())); // 消息含 "Circular reference detected at $.a.b"
throw;
}
// ❌ 不推荐:靠调大 max_depth 绕过“深度超限”,循环数据只会换个方式失败
json_encode_options opts;
opts.max_depth = 512; // 循环引用依然会被检测并抛异常,问题在数据不在深度
相关文档
- mc::variant 文档 - 通用变量类型系统
- mc::dict 文档 - 键值对集合
- json_wrapper 文档 - 基于白泽 C JSON 库的封装(另一模块)