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; // 循环引用依然会被检测并抛异常,问题在数据不在深度

相关文档