更多 C++ 文章见《修远之路(C++集萃)》专栏
nlohmann 是基于 Tagged Union + SAX/DOM 双路径解析的 Header-Only C++ JSON 库,通过 ADL (Argument-Dependent Lookup, 实参依赖查找 )实现零侵入式类型序列化。让 JSON 操作像 STL 容器一样自然,同时保留足够的扩展性以支撑二进制格式(CBOR/MessagePack/BSON/UBJSON)解析。
| 能力 | 适用场景 | 不适用场景 |
|---|---|---|
| STL 格式访问与遍历 | 配置文件读写、HTTP JSON 响应构建 | 流式增量解析(需 SAX 手动实现) |
| ADL 自动类型转换 | 结构体与 JSON 双向映射 | 超大规模 JSON(>100MB)零拷贝解析 |
| JSON Pointer (RFC 6901) | 深层嵌套路径定位 | 频繁路径查询(无缓存机制) |
| JSON Patch (RFC 6902) | 运行时动态修改 JSON 文档 | 高并发修改(非线程安全) |
架构与流程
核心模块
| 模块 | 核心职责 | 输入 → 输出 |
|---|---|---|
| Lexer | 将字节流切分为 Token 流 | 原始字节 →token_type枚举 |
| Parser | 递归下降语法分析,驱动 SAX 事件 | Token 流 → SAX 事件调用 |
| json_sax | 定义解析事件协议(纯虚接口) | 事件信号 →bool继续/终止 |
| json_value | Tagged Union 存储 JSON 值 | value_t标签 + 联合体 → 具体值 |
| adl_serializer | ADL 实现类型转换 | basic_json↔ 自定义类型 |
| serializer | DOM 递归序列化为文本/二进制 | basic_json→ 字节流 |
解析流程
原理与设计
关键抽象与机制
Tagged Union
Tagged Union —value_t+json_value;这是整个库最核心的数据抽象。
basic_json内部仅持有两个成员:
value_t m_type=value_t::null;// 1 byte discriminatorjson_value m_value={};// tagged unionjson_value是一个裸union,其中object/array/string/binary以指针存储,而boolean/number_*直接内联:
unionjson_value{object_t*object;// heap-allocatedarray_t*array;// heap-allocatedstring_t*string;// heap-allocatedbinary_t*binary;// heap-allocatedboolean_t boolean;// inlinenumber_integer_t number_integer;// inlinenumber_unsigned_t number_unsigned;// inlinenumber_float_t number_float;// inline};标量类型直接内联避免堆分配;复合类型用指针使sizeof(basic_json)保持固定(通常 16 字节:1 字节 type + padding + 8 字节 union),与std::vector<basic_json>的连续存储兼容。
SAX 双路径解析
Parser 本身不直接构建 DOM,而是通过json_sax接口发射事件。库内置三种 SAX 消费者:
json_sax_dom_parser:无条件构建完整 DOMjson_sax_dom_callback_parser:支持用户回调过滤,可跳过不需要的子树json_sax_acceptor:仅校验语法合法性,不构建任何数据结构
这种设计使得同一套 Parser/Lexer 基础设施可服务于"解析并构建"、“仅校验”、"选择性构建"三种场景,无需修改解析器代码。
ADL 类型转换
adl_serializer通过 Argument-Dependent Lookup 实现零侵入式序列化。当json.get<MyType>()被调用时,查找链为:
adl_serializer::from_json(j, val)- →
::nlohmann::from_json(j, val)(ADL 在nlohmann命名空间查找) - → 用户在
MyType所在命名空间提供的from_json重载
用户无需修改MyType定义,无需继承任何基类,只需在同命名空间提供自由函数即可。
核心设计
Header-Only
选择 Header-Only 最大化了易用性——#include <nlohmann/json.hpp>即可使用,无需链接。代价是编译耗时:单文件约 25000 行(v3.9.1),每个翻译单元包含时均需完整编译。v3.11+ 提供了json_fwd.hpp前置声明以缓解前向引用场景的编译开销。
指针存储
object/array/string/binary在 union 中以指针存储而非值存储。这是为了:
- 控制
sizeof(basic_json)固定为 16 字节,保证数组连续内存布局 - 移动语义仅需交换指针,无需深拷贝
- 空值(null)不分配堆内存
代价是每次访问复合类型都需一次指针间接寻址,且小字符串(SSO 优化)的优势被指针分配开销抵消。
SAX 事件驱动
SAX 中间层引入了虚函数调用开销(每个 JSON 值至少一次虚调用),但换来了:
- 回调过滤能力(
parser_callback_t) - 格式无关的解析管道(同一 SAX 接口服务 JSON/CBOR/MessagePack/BSON/UBJSON)
- 用户自定义 SAX 消费者的扩展能力
源码地图
json.hpp (single-header amalgamation, ~25000 lines) ├── value_t enum # 类型标签枚举,DOM 分发核心 ├── adl_serializer # ADL 类型转换策略 ├── json_sax # SAX 事件接口定义 ├── json_sax_dom_parser # SAX→DOM 构建器 ├── lexer_base / lexer # 词法分析器 ├── parser # 递归下降语法分析器 ├── json_pointer # RFC 6901 JSON Pointer ├── serializer # DOM→文本/二进制序列化器 ├── basic_json # 核心 DOM 类 └── json_value union # Tagged Union 存储层API 详解
常用 API
| API | 参数 | 说明 |
|---|---|---|
json::parse(Input) | 输入:字符串/流/迭代器对 | 从输入构建 DOM;抛parse_error |
json::sax_parse(Input, SAX*) | 输入 + SAX 消费者指针 | SAX 模式解析,返回bool成功/失败 |
json::dump(indent, ensure_ascii) | 缩进宽度、是否纯 ASCII | 序列化为字符串;抛type_error |
json::operator[](key) | 字符串键或整数索引 | 访问/创建元素;越界抛out_of_range |
json::get<T>() | 目标类型 | 类型转换;失败抛type_error |
json::get_ptr<T*>() | 指针类型 | 零拷贝获取内部指针;类型不匹配返回nullptr |
json::contains(key) | 键名 | 检查对象是否包含指定键 |
json::emplace(key, value) | 键值对 | 原位构造,避免临时对象 |
json::find(key) | 键名 | 返回迭代器;未找到返回end() |
json::patch(patch_doc) | RFC 6902 Patch 文档 | 应用 JSON Patch 操作 |
json::flatten()/unflatten() | 无 | 将嵌套对象扁平化为 JSON Pointer 路径键 |
样例
以下示例展示Engine 场景中典型的 JSON 构建、解析与错误处理:
#include<nlohmann/json.hpp>#include<spdlog/spdlog.h>#include<fstream>#include<stdexcept>usingjson=nlohmann::json;structEngineConfig{intthreadCount;floatsimilarityThreshold;std::string modelPath;};voidfrom_json(constjson&j,EngineConfig&cfg){j.at("threadCount").get_to(cfg.threadCount);j.at("similarityThreshold").get_to(cfg.similarityThreshold);j.at("modelPath").get_to(cfg.modelPath);}voidto_json(json&j,constEngineConfig&cfg){j=json{{"threadCount",cfg.threadCount},{"similarityThreshold",cfg.similarityThreshold},{"modelPath",cfg.modelPath}};}jsonloadAndValidate(conststd::string&filePath){std::ifstreamifs(filePath);if(!ifs.is_open()){throwstd::runtime_error("Cannot open config file: "+filePath);}try{json doc=json::parse(ifs);if(!doc.is_object()){throwstd::runtime_error("Config root must be an object");}if(!doc.contains("threadCount")||!doc["threadCount"].is_number_integer()){throwstd::runtime_error("Missing or invalid field: threadCount");}if(!doc.contains("similarityThreshold")||!doc["similarityThreshold"].is_number()){throwstd::runtime_error("Missing or invalid field: similarityThreshold");}if(!doc.contains("modelPath")||!doc["modelPath"].is_string()){throwstd::runtime_error("Missing or invalid field: modelPath");}returndoc;}catch(constjson::parse_error&e){spdlog::error("JSON parse error at byte {}: {}",e.byte,e.what());throw;}catch(constjson::type_error&e){spdlog::error("JSON type error: {}",e.what());throw;}}voidbuildResponse(){json response;response["status"]="ok";response["data"]=json::array();for(inti=0;i<3;++i){response["data"].emplace_back(json{{"id",i},{"score",0.95-i*0.1},{"label","person_"+std::to_string(i)}});}std::string payload=response.dump(2);spdlog::info("Response payload size: {} bytes",payload.size());}intmain(){try{json configDoc=loadAndValidate("config.json");EngineConfig cfg=configDoc.get<EngineConfig>();spdlog::info("Loaded config: threads={}, threshold={:.2f}, model={}",cfg.threadCount,cfg.similarityThreshold,cfg.modelPath);buildResponse();json patch=json::parse(R"([{"op":"replace","path":"/threadCount","value":8}])");json patched=configDoc.patch(patch);spdlog::info("Patched threadCount: {}",patched["threadCount"].get<int>());json flat=configDoc.flatten();spdlog::info("Flattened keys: {}",flat.dump());}catch(conststd::exception&e){spdlog::error("Fatal: {}",e.what());return1;}return0;}JSON与类直接映射
nlohmann/json 提供了三种将 JSON 对象与 C++ 结构体/类双向映射的机制,从全自动到全手动,覆盖不同控制粒度需求。
| 机制 | 声明位置 | 说明 |
|---|---|---|
| NLOHMANN_DEFINE_TYPE_INTRUSIVE | 类内部 | 修改类型定义,不支持默认值,适配 POD、字段名与 JSON 键一致 |
| NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE | 类外部 | 不修改类型定义,不支持默认值,适配不可修改第三方类、字段全公开 |
| ADL from_json/to_json 自由函数 | 类所在命名空间 | 不修改类型定义,支持默认值,支持校验、嵌套映射 |
| 手动静态方法 | 类内部 | 修改类型定义,支持默认值,支持完整控制、校验、日志、异常 |
宏映射(全自动)
适用于字段名与 JSON 键名完全一致、所有字段必须存在的简单结构体:
structPerson{std::string name;intage;NLOHMANN_DEFINE_TYPE_INTRUSIVE(Person,name,age)};INTRUSIVE版本需放在类内部(修改类定义),NON_INTRUSIVE版本放在类外部:
structPerson{std::string name;intage;};NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Person,name,age)宏展开后生成的from_json/to_json使用at()访问字段——键不存在时抛出out_of_range,无法提供默认值;字段顺序无关,但字段名必须精确匹配。
ADL 自由函数
在类所在命名空间提供from_json与to_json自由函数,nlohmann 通过 ADL 自动发现:
structEngineConfig{intthreadCount;floatsimilarityThreshold;std::string modelPath;};namespacenlohmann{voidfrom_json(constjson&j,EngineConfig&cfg){cfg.threadCount=j.at("threadCount").get<int>();cfg.similarityThreshold=j.at("similarityThreshold").get<float>();cfg.modelPath=j.at("modelPath").get<std::string>();}voidto_json(json&j,constEngineConfig&cfg){j=json{{"threadCount",cfg.threadCount},{"similarityThreshold",cfg.similarityThreshold},{"modelPath",cfg.modelPath}};}}关键细节:
j.at("key")— 键不存在抛out_of_range,用于必填字段j.value("key", default)— 键不存在返回默认值,用于可选字段j.contains("key")— 显式检查键是否存在,用于条件逻辑get_to()— 将 JSON 值写入已有变量,避免临时对象:j.at("x").get_to(cfg.x)
带默认值与校验的完整示例:
namespacenlohmann{voidfrom_json(constjson&j,EngineConfig&cfg){cfg.threadCount=j.value("threadCount",4);if(cfg.threadCount<=0){throwstd::invalid_argument("threadCount must be positive");}cfg.similarityThreshold=j.value("similarityThreshold",0.8f);cfg.modelPath=j.at("modelPath").get<std::string>();}}嵌套对象映射:当 JSON 包含嵌套结构时,递归调用get<>():
structDatabaseConfig{std::string host;intport;};structAppConfig{DatabaseConfig database;intlogLevel;};namespacenlohmann{voidfrom_json(constjson&j,DatabaseConfig&cfg){cfg.host=j.at("host").get<std::string>();cfg.port=j.value("port",5432);}voidfrom_json(constjson&j,AppConfig&cfg){cfg.database=j.at("database").get<DatabaseConfig>();cfg.logLevel=j.value("logLevel",1);}}枚举映射:nlohmann 不内置枚举转换,需手动实现。推荐使用std::unordered_map双向查找:
enumclassQueryType{Face,Body,Photo};conststd::unordered_map<std::string,QueryType>kQueryTypeMap={{"face",QueryType::Face},{"body",QueryType::Body},{"photo",QueryType::Photo}};namespacenlohmann{voidfrom_json(constjson&j,QueryType&t){autoname=j.get<std::string>();autoit=kQueryTypeMap.find(name);if(it==kQueryTypeMap.end()){throwstd::invalid_argument("Unknown QueryType: "+name);}t=it->second;}voidto_json(json&j,QueryType t){for(constauto&[name,val]:kQueryTypeMap){if(val==t){j=name;return;}}j=nullptr;}}手动静态方法
静态from_json成员函数模式,将反序列化逻辑封装为类的工厂方法,优势在于:
- 可传入上下文参数(如默认值、配置项)
- 可在构造过程中执行业务校验与日志
- 调用方显式选择反序列化入口,避免隐式转换
classSearchQueryArgs{public:inttopN;intthresholdMin;intthresholdMax;std::string imgType;staticSearchQueryArgsfrom_json(constnlohmann::json&inJson,intdefTopN){SearchQueryArgs tmp;tmp.topN=inJson.value("topN",defTopN);tmp.thresholdMin=inJson.value("thresholdMin",0);tmp.thresholdMax=inJson.value("thresholdMax",100);tmp.imgType=inJson.value("imgType","face");if(tmp.topN<=0||tmp.topN>1000){throwEngineParamError("topN out of range");}returntmp;}};ADL vs 手动静态方法选择指南:
| 维度 | ADL 自由函数 | 手动静态方法 |
|---|---|---|
| 调用方式 | 隐式:json.get<T>() | 显式:T::from_json(j, ctx) |
| 上下文传递 | 不支持(签名固定) | 支持额外参数 |
| 与 STL 算法兼容 | 是(get<vector<T>>()递归调用) | 需手动处理容器 |
| 代码发现性 | 低(ADL 查找隐式) | 高(显式调用) |
| 第三方库集成 | 适合(不修改类定义) | 不适合(需修改类) |
内置支持的类型
nlohmann/json 开箱即支持以下类型的自动转换,无需手写from_json/to_json:
| 类别 | 类型 |
|---|---|
| 标量 | bool,int,double,float,std::nullptr_t |
| 字符串 | std::string,const char* |
| 容器 | std::vector<T>,std::list<T>,std::deque<T>,std::array<T,N>,std::valarray<T> |
| 关联容器 | std::map<K,V>,std::unordered_map<K,V>,std::multimap<K,V> |
| 集合 | std::set<T>,std::unordered_set<T> |
| 元组 | std::tuple<Ts...>,std::pair<A,B> |
| 可选 | C++17std::optional<T>(v3.11+) |
| 智能指针 | std::unique_ptr<T>,std::shared_ptr<T> |
当T本身已支持from_json/to_json时,std::vector<T>等容器自动获得递归转换能力。
总结
nlohmann/json “通过接口解耦将单一职责推向极致”:Parser 不构建 DOM,SAX 不持有数据,Serializer 不感知输出目标,类型转换不侵入用户类型。每一层都只做一件事,层与层之间通过极简接口(json_sax的纯虚函数、output_adapter_t的类型擦除、ADL 的自由函数)连接。这种设计使得库在保持 API 极简的同时,具备了远超表面复杂度的扩展能力。
单头文件约 25000 行,在大型项目中每个包含它的翻译单元均需完整编译(编译耗时);推荐使用 v3.11+ 的json_fwd.hpp减少头文件依赖传播。
每个basic_json对象固定 16 字节,加上object_t/array_t/string_t的堆分配。对于大量小 JSON 对象(如仅含一个整数的对象),实际内存远超数据本身。如:一个{"a":1}约占 16(根对象)+ 堆上std::map开销 + 堆上std::string" "a"+ 16(值对象)≈ 100+ 字节。
basic_json非线程安全:不同线程访问同一json对象(即使只读)需外部同步;不同线程操作不同json对象是安全的;const对象的并发读取在实践中安全,但标准未做保证。