更多 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 双向映射超大规模 JSON100MB零拷贝解析JSON Pointer (RFC 6901)深层嵌套路径定位频繁路径查询无缓存机制JSON Patch (RFC 6902)运行时动态修改 JSON 文档高并发修改非线程安全架构与流程核心模块模块核心职责输入 → 输出Lexer将字节流切分为 Token 流原始字节 →token_type枚举Parser递归下降语法分析驱动 SAX 事件Token 流 → SAX 事件调用json_sax定义解析事件协议纯虚接口事件信号 →bool继续/终止json_valueTagged Union 存储 JSON 值value_t标签 联合体 → 具体值adl_serializerADL 实现类型转换basic_json↔ 自定义类型serializerDOM 递归序列化为文本/二进制basic_json→ 字节流解析流程原理与设计关键抽象与机制Tagged UnionTagged Union —value_tjson_value这是整个库最核心的数据抽象。basic_json内部仅持有两个成员value_t m_typevalue_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::vectorbasic_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.getMyType()被调用时查找链为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 详解常用 APIAPI参数说明json::parse(Input)输入字符串/流/迭代器对从输入构建 DOM抛parse_errorjson::sax_parse(Input, SAX*)输入 SAX 消费者指针SAX 模式解析返回bool成功/失败json::dump(indent, ensure_ascii)缩进宽度、是否纯 ASCII序列化为字符串抛type_errorjson::operator[](key)字符串键或整数索引访问/创建元素越界抛out_of_rangejson::getT()目标类型类型转换失败抛type_errorjson::get_ptrT*()指针类型零拷贝获取内部指针类型不匹配返回nullptrjson::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 构建、解析与错误处理#includenlohmann/json.hpp#includespdlog/spdlog.h#includefstream#includestdexceptusingjsonnlohmann::json;structEngineConfig{intthreadCount;floatsimilarityThreshold;std::string modelPath;};voidfrom_json(constjsonj,EngineConfigcfg){j.at(threadCount).get_to(cfg.threadCount);j.at(similarityThreshold).get_to(cfg.similarityThreshold);j.at(modelPath).get_to(cfg.modelPath);}voidto_json(jsonj,constEngineConfigcfg){jjson{{threadCount,cfg.threadCount},{similarityThreshold,cfg.similarityThreshold},{modelPath,cfg.modelPath}};}jsonloadAndValidate(conststd::stringfilePath){std::ifstreamifs(filePath);if(!ifs.is_open()){throwstd::runtime_error(Cannot open config file: filePath);}try{json docjson::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_errore){spdlog::error(JSON parse error at byte {}: {},e.byte,e.what());throw;}catch(constjson::type_errore){spdlog::error(JSON type error: {},e.what());throw;}}voidbuildResponse(){json response;response[status]ok;response[data]json::array();for(inti0;i3;i){response[data].emplace_back(json{{id,i},{score,0.95-i*0.1},{label,person_std::to_string(i)}});}std::string payloadresponse.dump(2);spdlog::info(Response payload size: {} bytes,payload.size());}intmain(){try{json configDocloadAndValidate(config.json);EngineConfig cfgconfigDoc.getEngineConfig();spdlog::info(Loaded config: threads{}, threshold{:.2f}, model{},cfg.threadCount,cfg.similarityThreshold,cfg.modelPath);buildResponse();json patchjson::parse(R([{op:replace,path:/threadCount,value:8}]));json patchedconfigDoc.patch(patch);spdlog::info(Patched threadCount: {},patched[threadCount].getint());json flatconfigDoc.flatten();spdlog::info(Flattened keys: {},flat.dump());}catch(conststd::exceptione){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(constjsonj,EngineConfigcfg){cfg.threadCountj.at(threadCount).getint();cfg.similarityThresholdj.at(similarityThreshold).getfloat();cfg.modelPathj.at(modelPath).getstd::string();}voidto_json(jsonj,constEngineConfigcfg){jjson{{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(constjsonj,EngineConfigcfg){cfg.threadCountj.value(threadCount,4);if(cfg.threadCount0){throwstd::invalid_argument(threadCount must be positive);}cfg.similarityThresholdj.value(similarityThreshold,0.8f);cfg.modelPathj.at(modelPath).getstd::string();}}嵌套对象映射当 JSON 包含嵌套结构时递归调用get()structDatabaseConfig{std::string host;intport;};structAppConfig{DatabaseConfig database;intlogLevel;};namespacenlohmann{voidfrom_json(constjsonj,DatabaseConfigcfg){cfg.hostj.at(host).getstd::string();cfg.portj.value(port,5432);}voidfrom_json(constjsonj,AppConfigcfg){cfg.databasej.at(database).getDatabaseConfig();cfg.logLevelj.value(logLevel,1);}}枚举映射nlohmann 不内置枚举转换需手动实现。推荐使用std::unordered_map双向查找enumclassQueryType{Face,Body,Photo};conststd::unordered_mapstd::string,QueryTypekQueryTypeMap{{face,QueryType::Face},{body,QueryType::Body},{photo,QueryType::Photo}};namespacenlohmann{voidfrom_json(constjsonj,QueryTypet){autonamej.getstd::string();autoitkQueryTypeMap.find(name);if(itkQueryTypeMap.end()){throwstd::invalid_argument(Unknown QueryType: name);}tit-second;}voidto_json(jsonj,QueryType t){for(constauto[name,val]:kQueryTypeMap){if(valt){jname;return;}}jnullptr;}}手动静态方法静态from_json成员函数模式将反序列化逻辑封装为类的工厂方法优势在于可传入上下文参数如默认值、配置项可在构造过程中执行业务校验与日志调用方显式选择反序列化入口避免隐式转换classSearchQueryArgs{public:inttopN;intthresholdMin;intthresholdMax;std::string imgType;staticSearchQueryArgsfrom_json(constnlohmann::jsoninJson,intdefTopN){SearchQueryArgs tmp;tmp.topNinJson.value(topN,defTopN);tmp.thresholdMininJson.value(thresholdMin,0);tmp.thresholdMaxinJson.value(thresholdMax,100);tmp.imgTypeinJson.value(imgType,face);if(tmp.topN0||tmp.topN1000){throwEngineParamError(topN out of range);}returntmp;}};ADL vs 手动静态方法选择指南维度ADL 自由函数手动静态方法调用方式隐式json.getT()显式T::from_json(j, ctx)上下文传递不支持签名固定支持额外参数与 STL 算法兼容是getvectorT()递归调用需手动处理容器代码发现性低ADL 查找隐式高显式调用第三方库集成适合不修改类定义不适合需修改类内置支持的类型nlohmann/json 开箱即支持以下类型的自动转换无需手写from_json/to_json类别类型标量bool,int,double,float,std::nullptr_t字符串std::string,const char*容器std::vectorT,std::listT,std::dequeT,std::arrayT,N,std::valarrayT关联容器std::mapK,V,std::unordered_mapK,V,std::multimapK,V集合std::setT,std::unordered_setT元组std::tupleTs...,std::pairA,B可选C17std::optionalT(v3.11)智能指针std::unique_ptrT,std::shared_ptrT当T本身已支持from_json/to_json时std::vectorT等容器自动获得递归转换能力。总结nlohmann/json “通过接口解耦将单一职责推向极致”Parser 不构建 DOMSAX 不持有数据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对象的并发读取在实践中安全但标准未做保证。