首页
/ 深入理解 JSON for Modern C++ 中 nlohmann::adl_serializer::from_json:JSON 值到自定义类型的反序列化机制

深入理解 JSON for Modern C++ 中 nlohmann::adl_serializer::from_json:JSON 值到自定义类型的反序列化机制

2026-09-05 13:06:31作者:侯霆垣

在 JSON for Modern C++(nlohmann/json)中,将 basic_json 值转换为用户自定义类型(UDT)的核心入口,就是序列化器结构体 nlohmann::adl_serializer 提供的静态函数 from_json。本文基于仓库中 from_json 官方 API 文档 展开,完整介绍其两个重载的函数签名、参数与返回值约定、默认/非默认可构造类型两种场景下的用法示例,并结合 adl_serializer.hppjson.hpp 的源码实现与 单元测试,剖析 get() 是如何自动分派到正确重载的底层机制,帮助你在项目中正确编写自定义类型的 from_json 转换函数。

一、from_json 在库中的定位

nlohmann::adl_serializerbasic_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. 重载 (1):无返回值(void)——转换结果通过输出参数 val 回传;
  2. 重载 (2):返回 JSON 值 j 转换为 TargetType 后的值(按值返回)。

两个重载的选取规则(源码注释与文档一致):

  1. 重载 (1) 被选中的前提TargetType 是默认可构造(DefaultConstructible)类型,且存在形如 void from_json(const basic_json&, TargetType&) 的自由函数;
  2. 重载 (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>:检测序列化器是否存在返回 voidfrom_json(const basic_json&, T&) 形式;
  • has_non_default_from_json<BasicJsonType, T>:检测是否存在返回 Tfrom_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

该示例有两处关键信息:

  1. 签名变化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);
  2. 完全特化的副作用(官方特别用注释强调的坑):一旦你为 ns::person 完全特化adl_serializer,默认模板中的 to_json 转发就不再适用于该类型——必须同时提供 to_json 方法,否则 person 将无法被转换回 JSON。这也是 单元测试 unit-regression2.cpp 中针对 issue #2574 的 adl_serializer<NonDefaultConstructible> 特化只写 from_json 的场景能被验证的前提之一;而更规范的完整双向特化可参考 unit-udt.cppadl_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_jsonTbasic_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 序列化路径的内存行为回归测试。

八、版本与相关文档

小结

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 反序列化能力。

登录后查看全文
热门项目推荐
相关项目推荐