如何用 fmt 格式化输出 nlohmann/json 以及 fmt 11.1.0 之后 format_as 失效的应对方法
2026/9/13 7:23:13 网站建设 项目流程

如何用 fmt 格式化输出 nlohmann/json 以及 fmt 11.1.0 之后 format_as 失效的应对方法

【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json

任务很具体:你手里有nlohmann/json的值,想把它交给 fmt 的fmt::format/fmt::print直接格式化,而不是先手动dump()再拼字符串。本文基于 JSON for Modern C++ 仓库的文档给出两条真实存在的路线:库自带的format_as()定制点(对 fmt 10.0.0–11.0.2 生效),以及在 fmt 11.1.0 及更新版本上该定制点失效后、文档给出的fmt::formatter特化替代方案。适用前提是库版本 3.13.0 或更新——format_asstd::formatter<basic_json>都是在 3.13.0 中加入的。

先判断你用的是哪个 fmt 版本

这是整篇文章的分水岭,文档(format_as 与 FAQ)给出的版本边界一致:

fmt 版本format_as()的效果推荐做法
10.0.0 – 11.0.2生效:fmt::format("{}", j)通过参数依赖查找(ADL)找到format_as并调用直接使用库自带的format_as
11.1.0 及以上不生效:fmt 把自动拾取的format_as限定为返回算术类型的重载,format_as返回std::string,因此只是不被使用,不会产生编译错误定义自己的fmt::formatter特化(见下文 recipe)

注意"失效"的表现是静默的:代码照样编译通过,只是输出不再来自format_as。如果你升级 fmt 到 11.1.0 后发现fmt::format的行为变了(例如退化成经过operator ValueType()的歧义或异常路径),先核对版本,再决定走哪条路线。

路线一:库自带的 format_as(fmt 10.0.0 – 11.0.2)

format_as的声明为:

template <typename BasicJsonType> std::string format_as(const BasicJsonType& j);

文档给出的可能实现就是一行return j.dump();,要点如下(均出自 format_as 文档):

  • 返回值与dump()相同,即紧凑序列化(无缩进空白);要美化输出,这条路线做不到,需要用下一节的fmt::formatter特化或 C++20 的std::formatter<basic_json>
  • 它对包含该翻译单元的fmt调用没有任何副作用:只有当调用方的翻译单元同时包含fmt并调用fmt::format/fmt::print作用于 JSON 值时才有意义。
  • 异常安全:强保证——抛异常时不改变任何 JSON 值;若 JSON 内存储的字符串不是 UTF-8 编码,会抛出type_error.316

文档示例(examples/format_as.cpp)直接展示了该函数按 ADL 方式被找到:

#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; }

文档示例输出(见 examples/format_as.output):

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

上面是直接调用format_as(j)的写法;在你的 fmt 10.0.0–11.0.2 工程里,把j作为参数传入fmt::format("{}", j)fmt::print("{}", j),fmt 会以同样的 ADL 机制找到它,输出即为j.dump()的结果。

路线二:fmt 11.1.0 之后——定义 fmt::formatter 特化

文档明确说:库本身不随包附带这份fmt::formatter<nlohmann::json>特化,因为那会让fmt成为构建依赖(背景见 FAQ 的 "Using JSON values withstd::formatorfmt" 条目)。但这份 recipe 被原样放在库自己的测试工程里,通过 CMakeFetchContent拉取真实的 fmt 版本进行编译和运行验证,与std::formatter<basic_json>保持同步。完整可编译版本见 tests/fmt_formatter/project/main.cpp。

recipe 本体(文档以formatter_recipe片段引用的部分):

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); } };

使用时需要把这段特化放进包含#include <fmt/format.h>#include <nlohmann/json.hpp>的翻译单元(完整 include 见 tests/fmt_formatter/project/main.cpp 开头)。格式说明符的语义与std::formatter<basic_json>对齐(见 std::formatter<basic_json> 文档):

格式串效果
{}紧凑输出,等价dump()
{:#}美化输出,缩进 4,等价dump(4)
{:2}美化输出,缩进 2,等价dump(2)(width 单独出现即隐含美化)
{:#2}等价dump(2)
{:.>#}缩进 4、缩进字符改为.,等价dump(4, '.')

其余任何规格组件(如{:x})会抛出fmt::format_error,测试工程里专门断言了这一点。

验证方式

文档给出的验证手段有两类,可直接对照:

  1. 库自测的断言方式(tests/fmt_formatter/project/main.cpp 的main函数):
const nlohmann::json j = {{"foo", 1}, {"bar", {1, 2, 3}}}; assert(fmt::format("{}", j) == j.dump()); assert(fmt::format("{:#}", j) == j.dump(4)); assert(fmt::format("{:2}", j) == j.dump(2)); assert(fmt::format("{:#2}", j) == j.dump(2)); assert(fmt::format("{:.>#}", j) == j.dump(4, '.'));

即:每一种格式串的输出都应与对应参数的j.dump(...)相等,{:#}对应dump(4),以此类推。

  1. 构建验证:测试工程 tests/fmt_formatter/project/CMakeLists.txt 用FetchContent拉取 json 仓库与 fmt(测试中固定GIT_TAG 12.2.0,CMake 要求 3.14+,C++17),链接nlohmann_json::nlohmann_jsonfmt::fmt,并在POST_BUILD阶段直接运行可执行文件——断言不成立时构建本身就失败。你本地若已有仓库与 CMake 3.19+,可以用同样的方式配置该示例工程:
cmake -S tests/fmt_formatter/project -B build cmake --build build

cmake --build会自动执行POST_BUILD中的测试运行;仓库内正式测试链还要求git,见 tests/fmt_formatter/CMakeLists.txt。)

附带排查:不定义任何 formatter 时出现歧义重载

FAQ(Using JSON values withstd::formatorfmt)提到一个相关症状:如果你没有任何fmt::formatter<json>特化在场,把 JSON 值直接传给fmt::format/fmt::print会得到 ambiguous-overload 编译错误。原因是 fmt 拾取了basic_json隐式的operator ValueType()转换运算符(对应上游 issue #964 与 #958)。规避方式是在包含库之前关闭隐式转换:

#define JSON_USE_IMPLICIT_CONVERSIONS 0 #include <nlohmann/json.hpp>

该宏的默认值是 1(开启隐式转换),文档同时说明下一个大版本将默认关闭(见 JSON_USE_IMPLICIT_CONVERSIONS);用 CMake 集成时也可用选项JSON_ImplicitConversions控制。

限制与下一步

  • format_as路线只覆盖紧凑输出;美化输出、缩进宽度与缩进字符只能来自路线二的fmt::formatter特化(或 C++20 下的std::formatter<basic_json>,其前提是标准库提供<format>且未关闭JSON_HAS_STD_FORMAT宏,见 JSON_HAS_STD_FORMAT)。
  • 两条路线的底层都是dump(),因此dump()的异常行为同样适用:默认 strict 模式下,JSON 内非 UTF-8 字符串会抛type_error.316;如需替换而非抛异常,使用dump(-1, ' ', false, json::error_handler_t::replace)这类非严格错误处理器(FAQ "Serializing untrusted or invalid UTF-8" 条目)。
  • recipe 中的特化模板参数写死为nlohmann::json;如果你使用basic_json的其他特化(如ordered_json),需要按同一逻辑改为对应类型。

【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询