JSON for Modern C++:format_as 与 fmt 库的 ADL 自定义格式化集成
本文围绕 JSON for Modern C++ 3.13.0 新增的 format_as() 函数展开:它是库为 {fmt} 格式化生态提供的无依赖自定义点,能让 nlohmann::json 值被 fmt::format/fmt::print 直接格式化;读完本文,你将掌握它的调用方式与 ADL 查找原理、与 fmt 各版本之间的兼容性边界(10.0.0–11.0.2 生效、11.1.0 起失效),以及在新版 fmt 下自写 fmt::formatter 特化的完整可复制配方。
函数签名与定位
format_as 的完整签名为:
template <typename BasicJsonType>
std::string format_as(const BasicJsonType& j);
它是 {fmt}(fmtlib)库所使用的 format_as 自定义点(customization point)的实现。关键设计特点有两个:
- 零依赖:它不依赖任何
fmt头文件,是库自身头文件的一部分,定义在 include/nlohmann/json.hpp 中(单头文件版本 single_include/nlohmann/json.hpp 同步包含); - 惰性生效:只有在调用者的编译单元同时包含了
fmt头文件、并对 JSON 值调用fmt::format/fmt::print时才会被实际使用,否则它对编译产物毫无影响。
// include/nlohmann/json.hpp
/// @brief user-defined format_as function for JSON values (fmt <= 11.0.x support)
NLOHMANN_BASIC_JSON_TPL_DECLARATION
std::string format_as(const NLOHMANN_BASIC_JSON_TPL& j)
{
return j.dump();
}
模板参数、返回值与异常约定
- 模板参数:
BasicJsonType——basic_json的某个特化,因此nlohmann::json与nlohmann::ordered_json都能使用(源码通过NLOHMANN_BASIC_JSON_TPL_DECLARATION宏展开模板,保证对非默认实例化也能正确推导)。 - 返回值:包含 JSON 值序列化结果的
std::string,等价于dump()的默认调用(紧凑输出,无缩进)。 - 异常安全:强保证(strong guarantee)——若抛出异常,任何 JSON 值都不会被修改。
- 异常:当 JSON 值内部的字符串不是 UTF-8 编码时,抛出
type_error.316(该异常语义与dump()一致,参见 Serialization)。 - 复杂度:线性。
从源码实现看,其"可能的实现"(possible implementation)仅是一行转调:
template <typename BasicJsonType>
std::string format_as(const BasicJsonType& j)
{
return j.dump();
}
这意味着格式化能力完全复用了库既有的序列化路径:转义、UTF-8 校验、二进制表示等细节都由 dump() 处理,format_as 只是为 fmt 的 ADL 机制暴露了一个固定名字的入口。
示例:与 fmt::format 的集成
官方示例 docs/mkdocs/docs/examples/format_as.cpp 展示了库提供的 format_as() 如何通过参数依赖查找(ADL)被 fmt::format 找到:
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
// create a JSON value
json j = {{"one", 1}, {"two", 2}};
// format_as() is found via argument-dependent lookup, the same way
// fmt::format/fmt::print would find it
auto j_str = format_as(j);
std::cout << j_str << std::endl;
}
输出(见 docs/mkdocs/docs/examples/format_as.output):
{"one":1,"two":2}
注意示例中调用的是非限定名 format_as(j):这正是 fmt 查找该函数的机制——无限定调用、仅靠参数类型所在的 nlohmann 命名空间做 ADL。因此只要编译单元里包含了 <nlohmann/json.hpp> 和 <fmt/format.h>,fmt::format("{}", j) 就会自动走这条路径,无需任何显式注册。
fmt 版本兼容性:11.1.0 是一个关键分界
这是使用 format_as 时最需要注意的"版本依赖行为"(原文档以 warning 形式强调):
- fmt 10.0.0 至 11.0.2:能自动拾取返回
std::string的format_as重载,本函数生效; - fmt 11.1.0 起:fmt 把自动
format_as拾取限制为返回算术类型的重载,因此本函数在这些版本上不再起作用——它只是被忽略,并不会产生编译错误。
从源码结构看,库在 include/nlohmann/json.hpp 的注释中直接标注了这一点(fmt <= 11.0.x support),说明作者对这一兼容性边界是有明确预期的。
新版 fmt 下的替代方案:自写 fmt::formatter 特化
如果使用 fmt ≥ 11.1.0,或者希望在任意 fmt 版本上获得与 std::formatter<basic_json> 相同的格式化说明符能力,应自行定义 fmt::formatter 特化,镜像同一套逻辑:
"{:#}"—— 以 4 空格缩进漂亮打印;- 宽度(width)设定缩进级别,如
"{:2}"、"{:#2}"; - fill-and-align 选择缩进字符,如
"{:.>#}"表示用.作为缩进字符并缩进 4 空格。
该配方(摘自 tests/fmt_formatter/project/main.cpp):
template <>
struct fmt::formatter<nlohmann::json>
{
// -1 means compact output (dump()); any value >= 0 means pretty-printed
// output with that many spaces (or indent_char) per level.
int indent = -1;
char indent_char = ' ';
constexpr auto parse(format_parse_context& ctx) -> format_parse_context::iterator
{
auto it = ctx.begin();
const auto end = ctx.end();
constexpr auto is_align = [](char c)
{
return c == '<' || c == '>' || c == '^';
};
// [[fill] align] - repurposed here to pick a custom indent character
if (it != end && it + 1 != end && is_align(it[1]))
{
indent_char = *it;
it += 2;
}
else if (it != end && is_align(*it))
{
++it;
}
// ['#'] - "alternate form", used here to request pretty-printing with a
// default indent of 4 (overridden by an explicit width below, if given)
if (it != end && *it == '#')
{
indent = 4;
++it;
}
// [width] - repurposed here to pick the indent size; a width without '#'
// implies pretty-printing since an indent otherwise has no meaning
if (it != end && *it >= '1' && *it <= '9')
{
indent = 0;
while (it != end && *it >= '0' && *it <= '9')
{
indent = (indent * 10) + (*it - '0');
++it;
}
}
if (it != end && *it != '}')
{
throw fmt::format_error("invalid format args for nlohmann::json");
}
return it;
}
auto format(const nlohmann::json& j, format_context& ctx) const
{
const auto dumped = j.dump(indent, indent_char);
return fmt::format_to(ctx.out(), "{}", dumped);
}
};
parse 阶段的解析顺序值得注意:先处理可选的 [[fill] align] 前缀(fill 字符被重新解释为缩进字符),再处理 #(请求漂亮打印,默认缩进 4),最后把宽度数字解析为缩进级别(无 # 时给出宽度即意味着启用漂亮打印)。任何残留字符都会触发 fmt::format_error。format 阶段最终调用的仍是 j.dump(indent, indent_char),与 std::formatter<basic_json> 的行为保持对齐。
关于为什么不把这份配方直接发布进库:那样做会使 fmt 变成构建依赖——库刻意保持 fmt-free(背景详见 FAQ:使用 std::format 或 fmt 格式化 JSON 值 一节)。但配方并非"纸上谈兵":它作为 tests/fmt_formatter 测试工程的一部分,由 CMake FetchContent 拉取一个真实的、当前的 fmt 发行版(tests/fmt_formatter/project/CMakeLists.txt 中固定 fmt 12.2.0)参与库自身的测试套件编译与运行,因此它能与 std::formatter<basic_json> 保持同步、且被验证为"确实可用"而不仅是示意代码。
测试验证:覆盖全部 JSON 值类型
单元测试 tests/src/unit-format-as.cpp 对 format_as 做了系统性验证,其断言形式均为 format_as(v) == v.dump(),覆盖:
- 全部基础值类型:null、boolean、string(含需转义的
foo"bar\baz\nqux与 UTF-8 多字节串äöü)、整型/无符号/浮点数字; - 结构化类型:空与非空 array、空与非空 object、嵌套混合结构
{{"foo",1},{"bar",{1,2,3}},{"baz",{{"a",null},{"b",false}}}}; - 扩展类型:空与非空 binary(含 subtype 42)、discarded 值;
- 非默认实例化:
ordered_json的 null/object/array 抽查,验证NLOHMANN_BASIC_JSON_TPL_DECLARATION对自定义basic_json特化的推导正确性; - ADL 可达性:通过一个仅以非限定名调用
format_as(j)的辅助模板(模拟 fmt 的真实调用方式)验证nlohmann::json与ordered_json都能被参数依赖查找找到。
fmt_formatter 集成测试则用运行时断言固化了配方行为(tests/fmt_formatter/project/main.cpp):"{}" 等价 dump()、"{:#}" 等价 dump(4)、"{:2}" 等价 dump(2)、"{:.>#}" 等价 dump(4, '.'),且非法说明符("{:x}")必须抛出 fmt::format_error;该断言在构建后由 CMake POST_BUILD 命令直接执行,失败即构建失败。
与 to_string、std::formatter 的关系及版本历史
format_as与库中已有的 user-definedto_string定位类似(两者实现同为转调dump()),区别在于服务对象:to_string服务于std::to_string风格的 ADL,format_as专门对接 fmt 的格式化管线;- 对 C++20 的
std::format,库在 3.13.0 起提供了原生支持std::formatter<basic_json>,其说明符体系({:#}、宽度、fill-and-align)正是上文fmt::formatter配方所镜像的对象; - 序列化总览可参考 Serialization;
- 版本历史:
format_as在 3.13.0 版本中加入。使用前提是至少构建 3.13.0 且调用方自行引入 fmt;在 fmt 11.1.0+ 下需按上文配方自行特化,或直接采用 C++20 的std::format路径。
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