nlohmann::basic_json::flatten:JSON for Modern C++ 中 JSON 扁平化与往返还原实战指南
本文基于 nlohmann::basic_json::flatten() 的官方 API 文档,结合仓库中的源码实现与单元测试,系统讲解该函数如何把一个任意嵌套的 JSON 值转换为"JSON Pointer → 原始值"的扁平对象:函数签名、返回类型、异常安全保证、时间复杂度、空容器这一关键边界行为,以及与之配对的 unflatten() 往返恢复机制,并逐层剖析递归实现与 RFC 6901 转义规则在源码中的落地方式。读完后,你可以直接在项目中安全使用 flatten()/unflatten() 做配置展平、差异对比或序列化预处理,并清楚其能力边界。
1. 函数总览
flatten() 的声明为(参见 API 文档):
basic_json flatten() const;
该函数的作用是:创建一个 JSON 对象,其键是 JSON Pointer(RFC 6901),其值全部是原始类型(primitive)。原始 JSON 值可以通过 unflatten() 函数恢复。官方文档对该函数的关键属性定义如下:
| 属性 | 说明 |
|---|---|
| 返回值 | 一个将 JSON Pointer 映射到原始值(primitive values)的对象 |
| 异常安全 | 强异常安全(Strong exception safety):若发生异常,原始值保持完好 |
| 复杂度 | 与 JSON 值的大小成线性关系 |
| 版本 | 自 2.0.0 版本起加入 |
| 相关函数 | unflatten(),其逆操作 |
需要注意的一个文档级重要说明(Notes)是:空对象和空数组会被扁平化为 null,并且无法通过 unflatten() 被正确重建。这是 flatten() 唯一会"丢失类型信息"的场景,后文将结合源码和测试详细说明。
2. 完整示例:嵌套对象如何被展平
官方文档给出的完整可运行示例位于 examples/flatten.cpp,它构造了一个包含浮点数、布尔、字符串、null、嵌套对象和数组的 JSON 值,然后调用 flatten():
#include <iostream>
#include <iomanip>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
// create JSON value
json j =
{
{"pi", 3.141},
{"happy", true},
{"name", "Niels"},
{"nothing", nullptr},
{
"answer", {
{"everything", 42}
}
},
{"list", {1, 0, 2}},
{
"object", {
{"currency", "USD"},
{"value", 42.99}
}
}
};
// call flatten()
std::cout << std::setw(4) << j.flatten() << '\n';
}
该示例的实际输出(见 flatten.output)是一个仅有一层深度的对象,每个键都是一条 JSON Pointer:
{
"/answer/everything": 42,
"/happy": true,
"/list/0": 1,
"/list/1": 0,
"/list/2": 2,
"/name": "Niels",
"/nothing": null,
"/object/currency": "USD",
"/object/value": 42.99,
"/pi": 3.141
}
可以观察到三类展平规则:
- 顶层键直接成为一段以
/开头的引用串,如/pi、/name; - 对象嵌套逐层拼接键名,如
/answer/everything、/object/currency; - 数组元素用数字下标作为引用串片段,如
/list/0、/list/1、/list/2。
所有叶值都是 null、字符串、布尔或数字,符合"值必须是原始类型"的约束。
3. 源码级实现剖析
flatten() 在头文件中的公开实现非常薄,位于 include/nlohmann/json.hpp:
basic_json flatten() const
{
basic_json result(value_t::object);
json_pointer::flatten("", *this, result);
return result;
}
它先创建一个空对象作为结果容器,然后调用 json_pointer::flatten 静态辅助函数,从引用串 ""(表示整个值)开始递归。真正的算法在 include/nlohmann/detail/json_pointer.hpp 的 flatten() 私有静态模板函数中,按值的类型分三类处理:
template<typename BasicJsonType>
static void flatten(const string_t& reference_string,
const BasicJsonType& value,
BasicJsonType& result)
{
switch (value.type())
{
case detail::value_t::array:
{
if (value.m_data.m_value.array->empty())
{
// flatten empty array as null
result[reference_string] = nullptr;
}
else
{
// iterate array and use index as a reference string
for (std::size_t i = 0; i < value.m_data.m_value.array->size(); ++i)
{
flatten(detail::concat<string_t>(reference_string, '/', std::to_string(i)),
value.m_data.m_value.array->operator[](i), result);
}
}
break;
}
case detail::value_t::object:
{
if (value.m_data.m_value.object->empty())
{
// flatten empty object as null
result[reference_string] = nullptr;
}
else
{
// iterate object and use keys as reference string
for (const auto& element : *value.m_data.m_value.object)
{
flatten(detail::concat<string_t>(reference_string, '/', detail::escape(element.first)), element.second, result);
}
}
break;
}
case detail::value_t::null:
// ... string / boolean / number_integer / number_unsigned /
// number_float / binary / discarded
default:
{
// add a primitive value with its reference string
result[reference_string] = value;
break;
}
}
}
从源码结构看,实现要点有三:
3.1 递归展平与线性复杂度
- 数组分支:逐个元素递归,引用串拼接
"/" + 下标; - 对象分支:逐个键值对递归,引用串拼接
"/" + escape(键名); - 其余所有类型(
null、字符串、布尔、整数、无符号整数、浮点数、二进制、discarded):直接把值写入result[reference_string]。
每个叶节点恰好被访问一次,字符串拼接的总长度等于所有引用串长度之和,这与文档声明的"线性复杂度"一致。由于结果对象在递归前已经创建、且 flatten() 是 const 成员函数,任何中途异常都不会影响调用者持有的原始值,这就是文档中"强异常安全"承诺的来源。
3.2 空容器为何变成 null
文档 Notes 里提到的边界行为在源码中有明确对应:空数组与空对象分支都不产生任何引用串子项,而是执行 result[reference_string] = nullptr。原因是空容器没有任何子项,无法派生出任何"指针 → 原始值"条目,若不显式写一个 null,该键在扁平结果中就会整体消失。代价是 unflatten() 无法区分"原本是 null"和"原本是空数组/空对象",只能统一还原为 null。
3.3 RFC 6901 键转义
对象键在拼接引用串前会经过 detail::escape() 处理,其实现位于 include/nlohmann/detail/string_escape.hpp:
template<typename StringType>
inline StringType escape(StringType s)
{
replace_substring(s, StringType{"~"}, StringType{"~0"});
replace_substring(s, StringType{"/"}, StringType{"~1"});
return s;
}
即按 RFC 6901 第 4 节规则:先把 ~ 替换为 ~0,再把 / 替换为 ~1(顺序不能颠倒)。这一转义保证含 /、~、"" 等特殊字符的键名在展平后仍可唯一、无损地被还原。
4. 单元测试对边界行为的验证
仓库的单元测试 tests/src/unit-json_pointer.cpp 中的 flatten SECTION 对上述规则做了完整验证,其中有两点值得特别关注:
特殊键名的转义正确性。 测试构造了包含空字符串键、/、~、~1 的对象,并断言展平结果:
json j = { /* ... */ {
"object", {
{"currency", "USD"},
{"value", 42.99},
{"", "empty string"},
{"/", "slash"},
{"~", "tilde"},
{"~1", "tilde1"}
}
} };
json j_flatten = {
// ...
{"/object/", "empty string"},
{"/object/~1", "slash"},
{"/object/~0", "tilde"},
{"/object/~01", "tilde1"}
};
CHECK(j.flatten() == j_flatten);
CHECK(j_flatten.unflatten() == j);
可以看到 "" 变成引用串尾部的 /,/ 变成 ~1,~ 变成 ~0,而原始键 ~1 转义后是 ~01——转义是逐字符进行的,~1 中的 ~ 先变为 ~0,再保留 1。
往返一致性与空容器例外。 同一测试段中还断言了显式往返 j.flatten().unflatten() == j,并对 null、数字、布尔、字符串等原始值的往返逐一验证;随后验证了文档所述边界:
// roundtrip for empty structured values (will be unflattened to null)
json const j_array(json::value_t::array);
CHECK(j_array.flatten().unflatten() == json());
json const j_object(json::value_t::object);
CHECK(j_object.flatten().unflatten() == json());
即空数组、空对象展平后再还原,得到的是 null 而非原来的空容器,与文档 Notes 完全一致。
5. 与 unflatten() 配对使用时的注意事项
unflatten()(见 unflatten 文档)是 flatten() 的逆操作,用于把扁平对象恢复为任意嵌套结构。从 detail/json_pointer.hpp 的实现看,它对输入有三重校验,任何手工构造或外部来源的扁平对象若不满足都可能抛异常:
- 值必须是对象,否则抛
type_error.314("only objects can be unflattened"); - 对象的每个值必须是原始类型,否则抛
type_error.315("values in object must be primitive"); - 键指向的嵌套发生冲突时抛
type_error.313(如{ "", 42, "/foo", 17 }这类整体值与子路径同时存在的对象); - 引用串中的数组下标不是数字时抛
parse_error.109(如/list/three)。
因此,在工程实践中建议遵循以下使用模式:
- 只把
flatten()的输出交给unflatten(),或确保外部扁平对象满足"对象 + 全原始值 + 合法 JSON Pointer 键"三条件; - 不要依赖扁平结果重建空容器:若业务上必须保留空数组/空对象类型,可在展平前自行标记(例如附加一个哨兵键),因为
null无法区分这两种来源; flatten()是只读操作:它是const成员函数,不修改原值,可以安全地用于对比两份嵌套 JSON 的差异(展平后对键值集合做 diff 即可),这也是该接口常见的应用场景之一。
6. 小结
nlohmann::basic_json::flatten() 以线性复杂度把任意嵌套 JSON 值转换为"JSON Pointer → 原始值"的扁平对象,具备强异常安全保证,自 2.0.0 版本提供。其实现核心是 detail/json_pointer.hpp 中按 array/object/primitive 三分支的递归算法,配合 string_escape.hpp 中的 RFC 6901 转义保证特殊键名无损展平。唯一需要记住的边界是空对象与空数组会被展平为 null 且无法还原其原始类型;除该情形外,恒有 j == j.flatten().unflatten(),可放心用于往返转换与结构化对比。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00