首页
/ JSON for Modern C++:format_as 与 fmt 库的 ADL 自定义格式化集成

JSON for Modern C++:format_as 与 fmt 库的 ADL 自定义格式化集成

2026-09-06 15:35:47作者:傅爽业Veleda

本文围绕 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::jsonnlohmann::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::stringformat_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_errorformat 阶段最终调用的仍是 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.cppformat_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::jsonordered_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-defined to_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 路径。
登录后查看全文
热门项目推荐
相关项目推荐