深入理解 JSON for Modern C++ 中 nlohmann::adl_serializer::from_json:JSON 值到自定义类型的反序列化机制
在 JSON for Modern C++(nlohmann/json)中,将 basic_json 值转换为用户自定义类型(UDT)的核心入口,就是序列化器结构体 nlohmann::adl_serializer 提供的静态函数 from_json。本文基于仓库中 from_json 官方 API 文档 展开,完整介绍其两个重载的函数签名、参数与返回值约定、默认/非默认可构造类型两种场景下的用法示例,并结合 adl_serializer.hpp、json.hpp 的源码实现与 单元测试,剖析 get() 是如何自动分派到正确重载的底层机制,帮助你在项目中正确编写自定义类型的 from_json 转换函数。
一、from_json 在库中的定位
nlohmann::adl_serializer 是 basic_json 模板的默认 JSONSerializer(第二个模板实参),负责在 JSON 值与 C++ 值之间做双向转换。其默认定义位于 include/nlohmann/adl_serializer.hpp,通过“非限定调用 + 尾置返回类型推导”的方式,把转换请求转发到命名空间中由用户提供的 ::nlohmann::from_json(...) 自由函数(C++17 及之后版本下,也可在类型所在命名空间内定义函数,由 ADL 找到)。
from_json 通常在以下时机被 basic_json 类调用:
- 显式调用
get<ValueType>()时; - 或者经由
basic_json的隐式转换运算符(conversion operators)间接触发。
也就是说,用户几乎不会直接手写 adl_serializer::from_json(...) 调用,而是通过 j.get<T>() 或隐式转换走到它;但它又是你为自定义类型编写反序列化逻辑时必须对接的接口形态。
二、两个重载的函数签名与语义
官方文档给出的完整声明如下(见 adl_serializer.hpp 第 26-42 行):
// (1) 适用于默认可构造类型:写入输出参数
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> {}))
参数与返回值约定
| 参数 | 方向 | 含义 |
|---|---|---|
j |
in | 要读取的 JSON 值(以完美转发形式传入) |
val |
out | 仅重载 (1) 存在:转换结果写入的目标对象引用 |
返回值:
- 重载 (1):无返回值(
void)——转换结果通过输出参数val回传; - 重载 (2):返回
JSON 值 j转换为TargetType后的值(按值返回)。
两个重载的选取规则(源码注释与文档一致):
- 重载 (1) 被选中的前提:
TargetType是默认可构造(DefaultConstructible)类型,且存在形如void from_json(const basic_json&, TargetType&)的自由函数; - 重载 (2) 被选中的前提:
TargetType不是默认可构造类型,此时通过形如TargetType from_json(const basic_json&)的函数直接构造并返回值。
这里有个值得注意的实现细节:重载 (2) 的 SFINAE 探测表达式里多传了一个 detail::identity_tag<TargetType>{}。查看 identity_tag.hpp 可知它只是一个空的“分派辅助结构体”(template <class T> struct identity_tag {};),其作用是:当类型本身没有默认构造能力时,编译器无法写出 TargetType{} 这样的探测语句,于是用 identity_tag<T> 作为占位实参来探测 from_json(j, <某种辅助对象>) 形式的调用是否可行,从而在编译期安全地区分两个重载。
重载 (1) 与 (2) 的转发实现
从 adl_serializer.hpp 的函数体看,两个重载都只是薄封装:
// 重载 (1) 的函数体
::nlohmann::from_json(std::forward<BasicJsonType>(j), val);
// 重载 (2) 的函数体
return ::nlohmann::from_json(std::forward<BasicJsonType>(j), detail::identity_tag<TargetType> {});
它们把调用转发到非限定名 ::nlohmann::from_json。因此你为自定义类型编写的自由函数,命名空间与签名需要能被这两个转发调用所匹配。
三、get() 是如何分派到这两个重载的
basic_json 的显式取值接口 get<ValueType>() 内部通过两个带优先级的 get_impl 重载完成分派,源码位于 json.hpp 第 1667-1718 行:
// 分支 A:默认可构造 + 存在 from_json(const json&, ValueType&)
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>) const noexcept(...)
{
auto ret = ValueType();
JSONSerializer<ValueType>::from_json(*this, ret);
return ret;
}
// 分支 B:存在 from_json(const json&) 返回 ValueType 的重载
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>) const
{
return JSONSerializer<ValueType>::from_json(*this);
}
两条分支的门禁特征来自 type_traits.hpp 第 118-155 行:
has_from_json<BasicJsonType, T>:检测序列化器是否存在返回void的from_json(const basic_json&, T&)形式;has_non_default_from_json<BasicJsonType, T>:检测是否存在返回T的from_json(const basic_json&)形式。源码注释明确说明该重载“用于非默认可构造的用户自定义类型”(used for non-default-constructible user-defined-types)。
另外文档还说明:如果序列化器同时提供两个重载,get() 会优先选择 ValueType from_json(const basic_json&) 这个按值返回的版本(对应 priority_tag<1> 分支的注释 "If json_serializer has both overloads of from_json(), this one is chosen")。同时,basic_json 自身的转换走 priority_tag<2> 的特殊分支,与 UDT 的 from_json 无关。
由此可以得到一条实践结论:重载 (1) 与 (2) 并不是你手动二选一,而是由你定义的 from_json 函数签名 + 目标类型是否默认可构造,共同在编译期自动分派的。
四、实战示例 (1):默认可构造类型
官方示例 from_json__default_constructible.cpp 演示了最常见的写法:为目标类型提供自由函数 from_json,当 get<ns::person>() 被调用时,adl_serializer 会自动转发进来。
#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;
}
输出(见 示例输出文件):
Ned Flanders (60) lives in 744 Evergreen Terrace
要点解析:
person有隐式默认构造函数,因此get()走“分支 A”:先auto ret = ValueType();默认构造,再调用from_json(*this, ret)填充,最后返回副本;- 转换函数签名
void from_json(const json&, person&)与重载 (1) 的-> decltype(..., void())推导匹配; - 函数体内逐字段使用
j.at(key).get_to(member)完成安全取值——at()在键缺失时会抛出json::out_of_range异常,get_to()则复用同一套序列化机制填充成员,两者都是推荐写法(对比operator[]会静默创建缺失键)。
五、实战示例 (2):非默认可构造类型
对于没有默认构造函数的类型(例如所有成员都必须显式初始化),无法先构造空对象再填充,必须直接按值构造并返回。官方示例 from_json__non_default_constructible.cpp 演示了通过完全特化 adl_serializer 来提供重载 (2) 的完整写法:
#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
该示例有两处关键信息:
- 签名变化:
static ns::person from_json(const json& j)返回类型本身,正好匹配重载 (2) 的返回类型推导-> decltype(::nlohmann::from_json(..., identity_tag<TargetType>{}));get()经由has_non_default_from_json特征选中priority_tag<1>分支,跳过默认构造步骤,直接return JSONSerializer<ValueType>::from_json(*this);。 - 完全特化的副作用(官方特别用注释强调的坑):一旦你为
ns::person完全特化了adl_serializer,默认模板中的to_json转发就不再适用于该类型——必须同时提供to_json方法,否则person将无法被转换回 JSON。这也是 单元测试 unit-regression2.cpp 中针对 issue #2574 的adl_serializer<NonDefaultConstructible>特化只写from_json的场景能被验证的前提之一;而更规范的完整双向特化可参考 unit-udt.cpp 中adl_serializer<std::shared_ptr<T>>、adl_serializer<std::unique_ptr<T>>等测试特化。
六、两种写法的选择建议
结合文档语义与 type_traits.hpp 的特征定义,可以归纳出如下决策表:
| 目标类型特性 | 推荐做法 | 对应重载 |
|---|---|---|
| 默认可构造,且希望保持最小侵入 | 在类型所在命名空间写自由函数 void from_json(const json&, T&)(或置于 nlohmann 命名空间,保证非限定调用可达) |
重载 (1) |
| 非默认可构造(所有构造函数均带参) | 完全特化 nlohmann::adl_serializer<T>,提供 static T from_json(const json&),并同时提供 to_json |
重载 (2) |
| 两种形态都可用 | 若同时提供两个重载,get() 优先选择按值返回的重载 (2)(源码注释确认) |
重载 (2) 优先 |
需要注意的边界:
- 特征
has_from_json/has_non_default_from_json在T为basic_json类型时被显式禁用(enable_if_t< !is_basic_json<T>::value >),以避免模板实例化无限递归——JSON 到 JSON 的转换走get_impl(detail::priority_tag<2>)的特殊分支; noexcept子句是透传的:你的from_json自由函数若可能抛异常(如示例中使用的j.at(...)),get()的noexcept规格会自动推导为false,异常会按@throw what json_serializer<ValueType> from_json() throws的约定向上传播,即 json.hpp get() 文档 所描述的“抛出所调用 from_json 方法抛出的异常”。
七、测试佐证
仓库测试目录验证了上述两种路径均可用:
- tests/src/unit-regression2.cpp:包含
adl_serializer<NonDefaultFromJsonStruct>(issue #1805)与adl_serializer<NonDefaultConstructible>(issue #2574)两个特化,均只实现from_json按值返回形式,用于回归验证非默认可构造类型的分派正确性; - tests/src/unit-udt.cpp:包含
adl_serializer完全特化(如std::shared_ptr<T>、std::unique_ptr<T>、udt::legacy_type)以及用户自定义序列化器替换basic_json默认模板实参(another_adl_serializer)的测试用例,说明该机制是可被替换的扩展点; - tests/src/unit-no-mem-leak-on-adl-serialize.cpp:针对 ADL 序列化路径的内存行为回归测试。
八、版本与相关文档
adl_serializer::from_json自版本 2.1.0 起加入(Version history: Added in version 2.1.0),当前仓库版本为 3.12.0(见 adl_serializer.hpp 文件头);- 反向操作(C++ 值 → JSON)请参见 to_json 文档 及其源码 adl_serializer.hpp 第 46-52 行;
- 触发本函数的上层接口为 basic_json::get(),
get_to()等成员填充接口可参考 get_to 文档。
小结
nlohmann::adl_serializer::from_json 是 JSON for Modern C++ 反序列化用户自定义类型的标准接口:重载 (1) 面向默认可构造类型,以 void 返回值 + 输出参数完成填充;重载 (2) 面向非默认可构造类型,以按值返回完成直接构造,其 SFINAE 探测借助 detail::identity_tag 实现编译期安全分派。get() 通过 has_from_json / has_non_default_from_json 特征与 priority_tag 优先级自动选择正确路径,用户只需按目标类型特性编写对应签名的 from_json(完全特化时切记补上 to_json),即可获得类型安全、异常语义清晰的 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 StartedRust0623
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