首页
/ JSON for Modern C++ 中 basic_json::dump 深入解析:四个序列化参数、转义规则与错误处理器实战

JSON for Modern C++ 中 basic_json::dump 深入解析:四个序列化参数、转义规则与错误处理器实战

2026-09-06 21:19:05作者:冯梦姬Eddie

本篇技术指南围绕 JSON for Modern C++(nlohmann/json)的序列化接口 basic_json::dump() 展开,完整讲解其 indentindent_charensure_asciierror_handler 四个参数的取值与行为,并结合源码剖析底层 serializer 的输出逻辑、UTF-8 校验状态机与二进制值的序列化格式。读完后你可以精确控制 JSON 输出的紧凑/美化形态、非 ASCII 字符的转义策略,以及面对非法 UTF-8 输入时的异常处理方案,避免在序列化不可信数据时因默认的 strict 模式而崩溃。

函数签名与设计动机

dump()nlohmann::basic_json 类的序列化函数,其签名定义在 json.hpp 中:

string_t dump(const int indent = -1,
              const char indent_char = ' ',
              const bool ensure_ascii = false,
              const error_handler_t error_handler = error_handler_t::strict) const;

该函数的设计有意模仿 Python 的 json.dumps(),目前支持其中的 indentensure_ascii 两个参数,并额外扩展了 C++ 特有的 indent_charerror_handler。返回值是一个包含该 JSON 值序列化结果的字符串。

参数详解

indent:缩进级别

  • indent >= 0:数组元素与对象成员将以该缩进级别进行美化打印(pretty print)。
  • indent == 0:仅插入换行符,不产生缩进。
  • indent == -1(默认值):生成最紧凑的表示,即单行无空格的输出。

indent_char:缩进字符

indent > 0 时,每个缩进级别使用的字符,默认为空格 ' ',可以替换为制表符 '\t' 等。

ensure_ascii:ASCII 强制转义

设为 true 时,输出中所有非 ASCII 字符都会被转义为 \uXXXX 序列,结果字符串仅由 ASCII 字符组成。这对需要保证输出可被纯 ASCII 管道安全传输的场景很有用。

error_handler:非法 UTF-8 的处理策略

反序列化/序列化过程中遇到非法 UTF-8 字节时的三种反应,枚举定义见 serializer.hpp

enum class error_handler_t
{
    strict,  ///< throw a type_error exception in case of invalid UTF-8
    replace, ///< replace invalid UTF-8 sequences with U+FFFD
    ignore   ///< ignore invalid UTF-8 sequences
};
取值 行为
strict(默认) 遇到解码错误时抛出异常
replace 用替换字符 U+FFFD 替换非法 UTF-8 序列
ignore 序列化时忽略非法序列:所有合法字节原样复制到输出,非法字节被丢弃

更多语义可参考 error_handler_t 参考页

警告(序列化不可信输入):当序列化的值可能包含非法或不可信的 UTF-8(例如直接来自网络输入的字节)时,dump() 在默认 strict 模式下会抛出 type_error.316。若不希望抛异常,请传入 error_handler_t::replace(替换为 U+FFFD)或 error_handler_t::ignore;对崩溃敏感路径上的调用方,应在选择非 strict 处理器之外,额外用 try/catch 包裹 dump()。项目 FAQ 中对此有专门说明,见 FAQ:Serializing untrusted or invalid UTF-8

源码实现剖析

从 dump() 到 serializer

basic_json::dump() 的实现非常薄(json.hpp):

string_t result;
serializer s(detail::output_adapter<char, string_t>(result), indent_char, error_handler);

if (indent >= 0)
{
    s.dump(*this, true, ensure_ascii, static_cast<unsigned int>(indent));
}
else
{
    s.dump(*this, false, ensure_ascii, 0);
}

return result;

可以看到全部工作委托给 detail::serializer

  • 输出通过 output_adapter 直接写入返回的 string_t,没有中间流开销;
  • indent >= 0 映射到 pretty_print = trueindent 的值作为 indent_step(每级缩进的字符数)传入;indent < 0 则走紧凑分支。

对象与数组的输出分支

真正的分派逻辑在 serializer::dump() 中,按 value_t 类型 switch:

  • 对象:空对象直接写 {};美化模式下先写 {\n,递归时每级缩进使用内部缓存的 indent_string(不足时按倍扩容),逐个成员输出 "key": value, 并以 ,\n 分隔;紧凑模式则输出 "key":value 且无空白。
  • 数组:逻辑对称——美化模式输出 [\n + 逐元素换行缩进 + 末尾回退缩进的 ],紧凑模式输出 [1,2,3] 形式。
  • 字符串:写前导引号,调用 dump_escaped(),再写收尾引号。

字符串转义规则(dump_escaped)

dump_escaped() 用一个 UTF-8 解码状态机(UTF8_ACCEPT / UTF8_REJECT / 中间态)逐字节扫描字符串,对已接受的码点执行如下映射:

码点 输出
0x08 退格 \b
0x09 水平制表 \t
0x0A 换行 \n
0x0C 换页 \f
0x0D 回车 \r
0x22 双引号 \"
0x5C 反斜杠 \\
其他控制字符(≤ 0x1F),或 ensure_ascii 开启时 ≥ 0x7F 的字符 \uXXXX;超过 U+FFFF 的码点拆成两个代理项 \uD800-\uDBFF + \uDC00-\uDFFF
其余字符 原样复制字节

其中 \uXXXX 的写入由 write_u_escape() 完成。这解释了示例中 "Hellö 😀!"ensure_ascii = true 时输出 "Hell\u00f6 \ud83d\ude00!" 的成因——emoji(U+1F600,超过 U+FFFF)被拆成 \ud83d\ude00 代理对。

非法 UTF-8 的处理路径

当解码状态机返回 UTF8_REJECT 时(serializer.hpp):

  • strict:抛出 type_error.316,异常消息形如 invalid UTF-8 byte at index 2: 0xA9(拼接出错位置索引与十六进制字节值);
  • ignore / replace:回退到最近一次成功接受的字节位置;replace 额外写入 U+FFFD(ensure_ascii 时写 \ufffd,否则写三字节 UTF-8 编码 0xEF 0xBF 0xBD),然后继续处理后续字符。

此外,字符串结束时若序列不完整,strict 模式抛出 incomplete UTF-8 string; last byte: 0x..serializer.hpp)。FAQ 中明确指出:由此类未捕获 type_error.316 导致的崩溃(如 CVE-2024-34363 的报告场景)属于使用问题而非库漏洞——因为 RFC 8259 要求 JSON 文本必须是合法 UTF-8,推荐做法就是传入非 strict 的错误处理器或捕获异常。

二进制值的序列化格式

自 3.8.0 起,binary 值在 dump() 输出中被表示为包含两个键的对象(见 serializer.hpp 的 value_t::binary 分支):

  • "bytes":字节数组,每个字节为整数;
  • "subtype":子类型整数,若该二进制值没有子类型则为 null

紧凑形态即 {"bytes":[...],"subtype":N}{"bytes":[...],"subtype":null};美化形态则按常规缩进展开两个键。

数字输出

整数与浮点数分别由 dump_integer()serializer.hpp)与 dump_float()serializer.hpp)输出;浮点路径对 IEEE 单/双精度类型有不同处理分支。

完整示例:观察各参数的实际效果

以下示例完整取自仓库中的 examples/dump.cpp,可直接复制运行(C++11 及以上,包含 single_include/nlohmann/json.hpp 单头文件版本):

#include <iostream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

int main()
{
    // create JSON values
    json j_object = {{"one", 1}, {"two", 2}};
    json j_array = {1, 2, 4, 8, 16};
    json j_string = "Hellö 😀!";

    // call dump()
    std::cout << "objects:" << '\n'
              << j_object.dump() << "\n\n"
              << j_object.dump(-1) << "\n\n"
              << j_object.dump(0) << "\n\n"
              << j_object.dump(4) << "\n\n"
              << j_object.dump(1, '\t') << "\n\n";

    std::cout << "arrays:" << '\n'
              << j_array.dump() << "\n\n"
              << j_array.dump(-1) << "\n\n"
              << j_array.dump(0) << "\n\n"
              << j_array.dump(4) << "\n\n"
              << j_array.dump(1, '\t') << "\n\n";

    std::cout << "strings:" << '\n'
              << j_string.dump() << '\n'
              << j_string.dump(-1, ' ', true) << '\n';

    // create JSON value with invalid UTF-8 byte sequence
    json j_invalid = "ä\xA9ü";
    try
    {
        std::cout << j_invalid.dump() << std::endl;
    }
    catch (const json::type_error& e)
    {
        std::cout << e.what() << std::endl;
    }

    std::cout << "string with replaced invalid characters: "
              << j_invalid.dump(-1, ' ', false, json::error_handler_t::replace)
              << "\nstring with ignored invalid characters: "
              << j_invalid.dump(-1, ' ', false, json::error_handler_t::ignore)
              << '\n';
}

运行输出(与仓库 examples/dump.output 一致):

objects:
{"one":1,"two":2}

{"one":1,"two":2}

{
"one": 1,
"two": 2
}

{
    "one": 1,
    "two": 2
}

{
	"one": 1,
	"two": 2
}

arrays:
[1,2,4,8,16]

[1,2,4,8,16]

[
1,
2,
4,
8,
16
]

[
    1,
    2,
    4,
    8,
    16
]

[
	1,
	2,
	4,
	8,
	16
]

strings:
"Hellö 😀!"
"Hell\u00f6 \ud83d\ude00!"
[json.exception.type_error.316] invalid UTF-8 byte at index 2: 0xA9
string with replaced invalid characters: "äü"
string with ignored invalid characters: "äü"

输出对照参数的效果一目了然:

  1. dump()dump(-1) 完全相同——默认就是最紧凑表示 {"one":1,"two":2}
  2. dump(0) 只加换行不加缩进;
  3. dump(4) 每级 4 个空格缩进;dump(1, '\t') 每级 1 个制表符;
  4. ensure_ascii = true"Hellö 😀!" 变为 "Hell\u00f6 \ud83d\ude00!"
  5. 含非法字节 0xA9 的字符串在 strict 模式下抛出 type_error.316(消息中给出了出错索引 2 与字节值 0xA9);replace 模式将其替换为 U+FFFD("äü" 中间的替换字符),ignore 模式直接丢弃("äü")。

异常安全、复杂度与相关接口

  • 异常安全:强保证(strong guarantee)——若抛出异常,任何 JSON 值都不会发生改变。
  • 可能抛出的异常:当 JSON 值内部存储的字符串不是 UTF-8 编码且 error_handlerstrict(默认)时,抛出 type_error.316,异常编号说明见 exceptions 参考
  • 时间复杂度:线性(与输入规模成线性关系;字符串转义同样是字符串长度的线性函数)。

如需将 JSON 直接写入流,可改用 operator<<(参见 operator<< 参考);关于序列化特性的整体背景(含与其他二进制格式 CBOR、MessagePack、BSON、UBJSON 的对比)见 Serialization 专题文章

版本历史

  • 1.0.0:dump() 引入。
  • 3.0.0:新增缩进字符 indent_char、选项 ensure_ascii 及异常。
  • 3.4.0:新增错误处理器(error_handler)。
  • 3.8.0:新增二进制值的序列化(bytes/subtype 对象表示)。

适用前提:以上参数行为、二进制序列化格式与 FAQ 中的错误处理建议均以当前仓库源码(include/nlohmann/detail/output/serializer.hppdocs/mkdocs/docs/api/basic_json/dump.md)为准,使用时应确认所用库版本不低于文中所述版本号。

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