JSON for Modern C++ 中 basic_json::dump 深入解析:四个序列化参数、转义规则与错误处理器实战
本篇技术指南围绕 JSON for Modern C++(nlohmann/json)的序列化接口 basic_json::dump() 展开,完整讲解其 indent、indent_char、ensure_ascii、error_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(),目前支持其中的 indent 与 ensure_ascii 两个参数,并额外扩展了 C++ 特有的 indent_char 与 error_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 = true,indent的值作为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: "äü"
输出对照参数的效果一目了然:
dump()与dump(-1)完全相同——默认就是最紧凑表示{"one":1,"two":2};dump(0)只加换行不加缩进;dump(4)每级 4 个空格缩进;dump(1, '\t')每级 1 个制表符;ensure_ascii = true时"Hellö 😀!"变为"Hell\u00f6 \ud83d\ude00!";- 含非法字节
0xA9的字符串在 strict 模式下抛出type_error.316(消息中给出了出错索引 2 与字节值 0xA9);replace模式将其替换为 U+FFFD("äü"中间的替换字符),ignore模式直接丢弃("äü")。
异常安全、复杂度与相关接口
- 异常安全:强保证(strong guarantee)——若抛出异常,任何 JSON 值都不会发生改变。
- 可能抛出的异常:当 JSON 值内部存储的字符串不是 UTF-8 编码且
error_handler为strict(默认)时,抛出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.hpp 与 docs/mkdocs/docs/api/basic_json/dump.md)为准,使用时应确认所用库版本不低于文中所述版本号。
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