JSON for Modern C++ 深入解析:adl_serializer 如何基于 ADL 实现用户自定义类型的默认序列化
本文以 nlohmann::adl_serializer 的 API 文档为核心,系统讲解 JSON for Modern C++ 库中默认序列化器的设计原理与用法:它如何通过参数依赖查找(Argument-Dependent Lookup,ADL)在用户类型的命名空间中定位 to_json/from_json 函数,从而把任意用户自定义类型(UDT)转换为 JSON 值或从 JSON 值还原。读完本文,你能够掌握默认序列化器的两个 from_json 重载的选择规则、to_json 的触发路径,并会编写默认可构造与非默认可构造类型两种典型场景的转换代码。
adl_serializer 是什么
在 JSON for Modern C++ 中,basic_json 类模板带有一个 JSONSerializer 模板参数,用于决定“JSON 值与 C++ 值之间如何互相转换”的策略。该参数默认就是 adl_serializer,其声明位于 json_fwd.hpp:
/*!
@brief default JSONSerializer template argument
This serializer ignores the template arguments and uses ADL
([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl))
for serialization.
*/
template<typename T = void, typename SFINAE = void>
struct adl_serializer;
随后在 basic_json 的模板参数列表中,JSONSerializer 缺省为 adl_serializer(见 json_fwd.hpp):
template<template<typename U, typename V, typename... Args> class ObjectType =
std::map,
template<typename U, typename... Args> class ArrayType = std::vector,
class StringType = std::string, class BooleanType = bool,
class NumberIntegerType = std::int64_t,
class NumberUnsignedType = std::uint64_t,
class NumberFloatType = double,
template<typename U> class AllocatorType = std::allocator,
template<typename T, typename SFINAE = void> class JSONSerializer =
adl_serializer,
class BinaryType = std::vector<std::uint8_t>, // cppcheck-suppress syntaxError
class CustomBaseClass = void>
class basic_json;
因此文档给出的抽象定义为:
template<typename, typename>
struct adl_serializer;
它是一个使用 ADL(Argument-Dependent Lookup,参数依赖查找)来选择 to_json/from_json 函数的序列化器——即从待转换类型所在命名空间中查找对应的 to_json/from_json 函数。其设计可简化理解为:
template<typename ValueType>
struct adl_serializer {
template<typename BasicJsonType>
static void to_json(BasicJsonType& j, const T& value) {
// calls the "to_json" method in T's namespace
}
template<typename BasicJsonType>
static void from_json(const BasicJsonType& j, T& value) {
// same thing, but with the "from_json" method
}
};
在 basic_json 内部,这个策略被具象为类型别名(见 json.hpp):
template<typename T, typename SFINAE>
using json_serializer = JSONSerializer<T, SFINAE>;
所有“JSON 值 → C++ 值”与“C++ 值 → JSON 值”的转换最终都经由 json_serializer 分发,而默认情形下它就是 adl_serializer。
源码级实现:三个成员函数
adl_serializer 的完整实现位于 adl_serializer.hpp,其主体结构为:
NLOHMANN_JSON_NAMESPACE_BEGIN
/// @sa https://json.nlohmann.me/api/adl_serializer/
template<typename ValueType, typename>
struct adl_serializer
{
/// @brief convert a JSON value to any value type
template<typename BasicJsonType, typename TargetType = ValueType>
static auto from_json(BasicJsonType && j, TargetType& val) noexcept(
noexcept(::nlohmann::from_json(std::forward<BasicJsonType>(j), val)))
-> decltype(::nlohmann::from_json(std::forward<BasicJsonType>(j), val), void())
{
::nlohmann::from_json(std::forward<BasicJsonType>(j), val);
}
/// @brief convert a JSON value to any value type
template<typename BasicJsonType, typename TargetType = ValueType>
static auto from_json(BasicJsonType && j) noexcept(
noexcept(::nlohmann::from_json(std::forward<BasicJsonType>(j), detail::identity_tag<TargetType> {})))
-> decltype(::nlohmann::from_json(std::forward<BasicJsonType>(j), detail::identity_tag<TargetType> {}))
{
return ::nlohmann::from_json(std::forward<BasicJsonType>(j), detail::identity_tag<TargetType> {});
}
/// @brief convert any value type to a JSON value
template<typename BasicJsonType, typename TargetType = ValueType>
static auto to_json(BasicJsonType& j, TargetType && val) noexcept(
noexcept(::nlohmann::to_json(j, std::forward<TargetType>(val))))
-> decltype(::nlohmann::to_json(j, std::forward<TargetType>(val)), void())
{
::nlohmann::to_json(j, std::forward<TargetType>(val));
}
};
NLOHMANN_JSON_NAMESPACE_END
从源码结构看,有三个值得注意的设计点:
- 两个模板参数均“被忽略”。结构体签名是
template<typename ValueType, typename>:第二个模板参数来自basic_json::json_serializer<T, SFINAE>的 SFINAE 槽位,实现中并不使用;ValueType也只作为默认实参传递给成员模板的TargetType。这与 json_fwd.hpp 注释中 “This serializer ignores the template arguments” 的说法一致。 - 成员函数本质是转发器。
adl_serializer本身不含任何转换逻辑,它只是以全限定形式::nlohmann::from_json(...)/::nlohmann::to_json(...)发起调用。由于调用表达式中出现了TargetType(即用户类型),编译器在 ADL 时会额外搜索该类型的命名空间,从而“顺带”发现用户自行编写的自由函数to_json/from_json——这正是 ADL 机制发挥作用的所在。 - SFINAE 与
noexcept双重透传。成员函数用-> decltype(...)控制自身是否参与重载决议(若库内不存在对应的::nlohmann::from_json重载,则该成员被剔除出候选集),同时用noexcept(noexcept(...))将底层函数的异常传播属性原样透出,保证上层get()等 API 的noexcept标注准确。
其中第二个 from_json 重载出现的 detail::identity_tag<TargetType> 是一个空标记类型,定义见 identity_tag.hpp:
template <class T> struct identity_tag {};
它携带类型信息但本身不带数据,用于让编译器“探测”是否存在形如 T from_json(const basic_json&) 的返回值风格重载(详见下文“非默认可构造类型”一节)。
from_json 成员函数:两个重载与选择规则
from_json 的完整签名如源码所示(文档版见 from_json.md),按用途分为两组:
// (1) 用于默认可构造的类型:结果写入输出参数 val
template<typename BasicJsonType, typename TargetType = ValueType>
static auto from_json(BasicJsonType && j, TargetType& val) noexcept(
noexcept(::nlohmann::from_json(std::forward<BasicJsonType>(j), val)))
-> decltype(::nlohmann::from_json(std::forward<BasicJsonType>(j), val), void())
// (2) 用于非默认可构造的类型:直接返回值
template<typename BasicJsonType, typename TargetType = ValueType>
static auto from_json(BasicJsonType && j) noexcept(
noexcept(::nlohmann::from_json(std::forward<BasicJsonType>(j), detail::identity_tag<TargetType> {})))
-> decltype(::nlohmann::from_json(std::forward<BasicJsonType>(j), detail::identity_tag<TargetType> {}))
选择规则(与官方文档一致):
- 重载 (1) 在目标类型默认可构造时被选中;
- 重载 (2) 在目标类型不可默认构造时被选中。
参数与返回值约定:
| 参数 | 方向 | 含义 |
|---|---|---|
j |
in | 待读取的 JSON 值 |
val |
out | 写入转换结果的目标值(仅重载 (1)) |
返回值:重载 (1) 无返回值(结果写入 val);重载 (2) 返回 j 转换后的 TargetType 值。
这个函数通常由 basic_json 的 get() 函数(显式调用或经由转换运算符隐式触发)调用。basic_json 内部确实提供了两档实现,见 json.hpp 的 get_impl:
template < typename ValueType,
detail::enable_if_t <
detail::is_default_constructible<ValueType>::value&&
detail::has_from_json<basic_json_t, ValueType>::value,
int > = 0 >
ValueType get_impl(detail::priority_tag<0> /*unused*/) const noexcept(noexcept(
JSONSerializer<ValueType>::from_json(std::declval<const basic_json_t&>(), std::declval<ValueType&>())))
{
auto ret = ValueType();
JSONSerializer<ValueType>::from_json(*this, ret);
return ret;
}
以及面向非默认可构造类型的特化分支(见 json.hpp):
template < typename ValueType,
detail::enable_if_t <
detail::has_non_default_from_json<basic_json_t, ValueType>::value,
int > = 0 >
ValueType get_impl(detail::priority_tag<1> /*unused*/) const noexcept(noexcept(
JSONSerializer<ValueType>::from_json(std::declval<const basic_json_t&>())))
{
return JSONSerializer<ValueType>::from_json(*this);
}
两档的 SFINAE 条件分别由 type_traits.hpp 中的特征检测提供:
has_from_json(第 118 行起):检测JSONSerializer<T>::from_json(json const&, udt&)是否存在;has_non_default_from_json(第 142 行起):检测JSONSerializer<T>::from_json(json const&)(返回T)是否存在,注释明确说明 “this overload is used for non-default-constructible user-defined-types”。
to_json 成员函数
template<typename BasicJsonType, typename TargetType = ValueType>
static auto to_json(BasicJsonType& j, TargetType && val) noexcept(
noexcept(::nlohmann::to_json(j, std::forward<TargetType>(val))))
-> decltype(::nlohmann::to_json(j, std::forward<TargetType>(val)), void())
参数约定(与文档一致):
j(out):待写入的 JSON 值;val(in):待读取的 C++ 值。
该函数通常由 basic_json 的构造函数调用。从源码看,构造路径正是如此(见 json.hpp):
JSONSerializer<U>::to_json(std::declval<basic_json_t&>(), ...); // SFINAE 探测
JSONSerializer<U>::to_json(*this, std::forward<CompatibleType>(val)); // 实际构造时调用
即当你执行 json j = myStruct; 时,构造函数经 JSONSerializer<U>::to_json 分发到 adl_serializer::to_json,最终通过 ADL 找到用户命名空间中的自由函数 to_json。是否存在可用的 to_json 同样由特征检测把关,见 type_traits.hpp 的 has_to_json。
实战示例一:默认可构造类型(ADL 自由函数)
官方示例 from_json__default_constructible.cpp 演示了如何为用户类型实现 from_json——当调用 get<ns::person>() 时,adl_serializer 会通过 ADL 找到该函数:
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace ns
{
// a simple struct to model a person
struct person
{
std::string name;
std::string address;
int age;
};
} // namespace ns
namespace ns
{
void from_json(const json& j, person& p)
{
j.at("name").get_to(p.name);
j.at("address").get_to(p.address);
j.at("age").get_to(p.age);
}
} // namespace ns
int main()
{
json j;
j["name"] = "Ned Flanders";
j["address"] = "744 Evergreen Terrace";
j["age"] = 60;
auto p = j.get<ns::person>();
std::cout << p.name << " (" << p.age << ") lives in " << p.address << std::endl;
}
输出(见 from_json__default_constructible.output):
Ned Flanders (60) lives in 744 Evergreen Terrace
这里的要点:person 结构体定义在 namespace ns 中,from_json 也必须定义在同一命名空间。调用链为 j.get<ns::person>() → get_impl(priority_tag<0>)(person 默认可构造且 has_from_json 为真)→ adl_serializer<ns::person>::from_json(j, ret) → ::nlohmann::from_json(j, ret)。最后一个表达式中出现了 ns::person&,ADL 会同时搜索 namespace ns,于是用户定义的 ns::from_json 被优先匹配。
实战示例二:非默认可构造类型(特化 adl_serializer)
当类型不可默认构造时,无法使用“先构造、再填充”的 (1) 重载。官方示例 from_json__non_default_constructible.cpp 演示了另一条路径——直接特化 adl_serializer,提供返回 T 的 from_json:
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace ns
{
// a simple struct to model a person (not default constructible)
struct person
{
person(std::string n, std::string a, int aa)
: name(std::move(n)), address(std::move(a)), age(aa)
{}
std::string name;
std::string address;
int age;
};
} // namespace ns
namespace nlohmann
{
template <>
struct adl_serializer<ns::person>
{
static ns::person from_json(const json& j)
{
return {j.at("name"), j.at("address"), j.at("age")};
}
// Here's the catch! You must provide a to_json method! Otherwise, you
// will not be able to convert person to json, since you fully
// specialized adl_serializer on that type
static void to_json(json& j, ns::person p)
{
j["name"] = p.name;
j["address"] = p.address;
j["age"] = p.age;
}
};
} // namespace nlohmann
int main()
{
json j;
j["name"] = "Ned Flanders";
j["address"] = "744 Evergreen Terrace";
j["age"] = 60;
auto p = j.get<ns::person>();
std::cout << p.name << " (" << p.age << ") lives in " << p.address << std::endl;
}
输出同样为:
Ned Flanders (60) lives in 744 Evergreen Terrace
从源码角度解释这条路径为何可行:person 不可默认构造,于是 get_impl(priority_tag<0>) 因 is_default_constructible 为假被 SFINAE 剔除;adl_serializer<ns::person> 特化提供的 T from_json(const json&) 使 has_non_default_from_json 成立,get_impl(priority_tag<1>) 被选中,执行 return JSONSerializer<ValueType>::from_json(*this);(见 json.hpp)。这也解释了 adl_serializer 第二个 from_json 重载中 identity_tag<TargetType> 的作用:库通过探测 from_json(j, identity_tag<TargetType>{}) 是否可调用,判断目标类型是否具备返回值风格的 from_json。
示例中注释特别强调了一个易踩的坑:对某类型完全特化 adl_serializer 之后,原模板中用于 ADL 分发的 to_json 一并被替换——因此必须同时手工提供 to_json,否则该类型将无法再转换为 JSON。
实战示例三:to_json 与隐式构造
to_json.cpp 演示了序列化方向的用法。当你执行构造函数 basic_json(ns::person)(即 json j = p;)时,adl_serializer 会调用用户命名空间中的 to_json:
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace ns
{
// a simple struct to model a person
struct person
{
std::string name;
std::string address;
int age;
};
} // namespace ns
namespace ns
{
void to_json(json& j, const person& p)
{
j = json{ {"name", p.name}, {"address", p.address}, {"age", p.age} };
}
} // namespace ns
int main()
{
ns::person p = {"Ned Flanders", "744 Evergreen Terrace", 60};
json j = p;
std::cout << j << std::endl;
}
输出(见 to_json.output):
{"address":"744 Evergreen Terrace","age":60,"name":"Ned Flanders"}
注意输出键按字典序排序,这是默认对象容器 std::map 的行为;若需要保持插入顺序,可换用 ordered_json(即 basic_json<nlohmann::ordered_map>),其 JSONSerializer 模板参数同样默认为 adl_serializer,用法完全一致。
to_json 一侧的库内支持函数集中在 to_json.hpp,from_json 一侧的内建重载(std::nullptr_t、std::optional、算术类型、字符串、容器等)集中在 from_json.hpp。用户自定义类型的自由函数与这些内建重载共同构成 ADL 的候选集:由于自由函数位于用户类型所在命名空间,对相应类型它总是优先于 nlohmann 命名空间内的同名重载被选中。
版本历史与延伸阅读
adl_serializer自 2.1.0 版本引入(与from_json、to_json成员函数的版本标注一致)。- 相关测试用例集中在 unit-udt.cpp(用户自定义类型转换)与 unit-conversions.cpp 等文件中,可进一步验证 ADL 转换行为。
- 成员函数完整文档:from_json、to_json。
小结:adl_serializer 本身逻辑极简——两个 from_json 重载加一个 to_json,全部只是对 ::nlohmann::from_json/::nlohmann::to_json 的 SFINAE 转发;它真正的价值在于作为 basic_json 的默认 JSONSerializer,借助 ADL 把序列化职责下放给用户类型所在的命名空间。理解“默认可构造走输出参数重载、非默认可构造走返回值重载”这一选择规则,以及完全特化时必须补齐 to_json 的坑,即可覆盖绝大多数 UDT ↔ JSON 转换场景。
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