首页
/ nlohmann::basic_json::flatten:JSON for Modern C++ 中 JSON 扁平化与往返还原实战指南

nlohmann::basic_json::flatten:JSON for Modern C++ 中 JSON 扁平化与往返还原实战指南

2026-09-06 14:04:42作者:胡唯隽

本文基于 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
}

可以观察到三类展平规则:

  1. 顶层键直接成为一段以 / 开头的引用串,如 /pi/name
  2. 对象嵌套逐层拼接键名,如 /answer/everything/object/currency
  3. 数组元素用数字下标作为引用串片段,如 /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.hppflatten() 私有静态模板函数中,按值的类型分三类处理:

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)。

因此,在工程实践中建议遵循以下使用模式:

  1. 只把 flatten() 的输出交给 unflatten(),或确保外部扁平对象满足"对象 + 全原始值 + 合法 JSON Pointer 键"三条件;
  2. 不要依赖扁平结果重建空容器:若业务上必须保留空数组/空对象类型,可在展平前自行标记(例如附加一个哨兵键),因为 null 无法区分这两种来源;
  3. 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(),可放心用于往返转换与结构化对比。

登录后查看全文
热门项目推荐
相关项目推荐