C++之现代易用JSON库nlohmann

更多 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 内部仅持有两个成员:

cpp 复制代码
value_t m_type = value_t::null;       // 1 byte discriminator
json_value m_value = {};               // tagged union

json_value 是一个裸 union,其中 object/array/string/binary 以指针存储,而 boolean/number_* 直接内联:

cpp 复制代码
union json_value {
    object_t* object;           // heap-allocated
    array_t* array;             // heap-allocated
    string_t* string;           // heap-allocated
    binary_t* binary;           // heap-allocated
    boolean_t boolean;          // inline
    number_integer_t number_integer;    // inline
    number_unsigned_t number_unsigned;  // inline
    number_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:无条件构建完整 DOM
  • json_sax_dom_callback_parser:支持用户回调过滤,可跳过不需要的子树
  • json_sax_acceptor:仅校验语法合法性,不构建任何数据结构

这种设计使得同一套 Parser/Lexer 基础设施可服务于"解析并构建"、"仅校验"、"选择性构建"三种场景,无需修改解析器代码。

ADL 类型转换

adl_serializer 通过 Argument-Dependent Lookup 实现零侵入式序列化。当 json.get<MyType>() 被调用时,查找链为:

  1. adl_serializer::from_json(j, val)
  2. ::nlohmann::from_json(j, val) (ADL 在 nlohmann 命名空间查找)
  3. → 用户在 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 消费者的扩展能力

源码地图

plain 复制代码
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 构建、解析与错误处理:

cpp 复制代码
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include <fstream>
#include <stdexcept>

using json = nlohmann::json;

struct EngineConfig {
    int threadCount;
    float similarityThreshold;
    std::string modelPath;
};

void from_json(const json& j, EngineConfig& cfg) {
    j.at("threadCount").get_to(cfg.threadCount);
    j.at("similarityThreshold").get_to(cfg.similarityThreshold);
    j.at("modelPath").get_to(cfg.modelPath);
}

void to_json(json& j, const EngineConfig& cfg) {
    j = json{
        {"threadCount", cfg.threadCount},
        {"similarityThreshold", cfg.similarityThreshold},
        {"modelPath", cfg.modelPath}
    };
}

json loadAndValidate(const std::string& filePath) {
    std::ifstream ifs(filePath);
    if (!ifs.is_open()) {
        throw std::runtime_error("Cannot open config file: " + filePath);
    }

    try {
        json doc = json::parse(ifs);
        if (!doc.is_object()) {
            throw std::runtime_error("Config root must be an object");
        }
        if (!doc.contains("threadCount") || !doc["threadCount"].is_number_integer()) {
            throw std::runtime_error("Missing or invalid field: threadCount");
        }
        if (!doc.contains("similarityThreshold") || !doc["similarityThreshold"].is_number()) {
            throw std::runtime_error("Missing or invalid field: similarityThreshold");
        }
        if (!doc.contains("modelPath") || !doc["modelPath"].is_string()) {
            throw std::runtime_error("Missing or invalid field: modelPath");
        }
        return doc;
    } catch (const json::parse_error& e) {
        spdlog::error("JSON parse error at byte {}: {}", e.byte, e.what());
        throw;
    } catch (const json::type_error& e) {
        spdlog::error("JSON type error: {}", e.what());
        throw;
    }
}

void buildResponse() {
    json response;
    response["status"] = "ok";
    response["data"] = json::array();

    for (int i = 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());
}

int main() {
    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 (const std::exception& e) {
        spdlog::error("Fatal: {}", e.what());
        return 1;
    }
    return 0;
}

JSON与类直接映射

nlohmann/json 提供了三种将 JSON 对象与 C++ 结构体/类双向映射的机制,从全自动到全手动,覆盖不同控制粒度需求。

机制 声明位置 说明
NLOHMANN_DEFINE_TYPE_INTRUSIVE 类内部 修改类型定义,不支持默认值,适配 POD、字段名与 JSON 键一致
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE 类外部 不修改类型定义,不支持默认值,适配不可修改第三方类、字段全公开
ADL from_json/to_json 自由函数 类所在命名空间 不修改类型定义,支持默认值,支持校验、嵌套映射
手动静态方法 类内部 修改类型定义,支持默认值,支持完整控制、校验、日志、异常

宏映射(全自动)

适用于字段名与 JSON 键名完全一致、所有字段必须存在的简单结构体:

cpp 复制代码
struct Person {
    std::string name;
    int age;
    NLOHMANN_DEFINE_TYPE_INTRUSIVE(Person, name, age)
};

INTRUSIVE 版本需放在类内部(修改类定义),NON_INTRUSIVE 版本放在类外部:

cpp 复制代码
struct Person {
    std::string name;
    int age;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Person, name, age)

宏展开后生成的 from_json/to_json 使用 at() 访问字段------键不存在时抛出 out_of_range,无法提供默认值;字段顺序无关,但字段名必须精确匹配。

ADL 自由函数

在类所在命名空间提供 from_jsonto_json 自由函数,nlohmann 通过 ADL 自动发现:

cpp 复制代码
struct EngineConfig {
    int threadCount;
    float similarityThreshold;
    std::string modelPath;
};

namespace nlohmann {
    void from_json(const json& 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>();
    }
    void to_json(json& j, const EngineConfig& 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)

带默认值与校验的完整示例:

cpp 复制代码
namespace nlohmann {
    void from_json(const json& j, EngineConfig& cfg) {
        cfg.threadCount = j.value("threadCount", 4);
        if (cfg.threadCount <= 0) {
            throw std::invalid_argument("threadCount must be positive");
        }
        cfg.similarityThreshold = j.value("similarityThreshold", 0.8f);
        cfg.modelPath = j.at("modelPath").get<std::string>();
    }
}

嵌套对象映射:当 JSON 包含嵌套结构时,递归调用 get<>()

cpp 复制代码
struct DatabaseConfig {
    std::string host;
    int port;
};

struct AppConfig {
    DatabaseConfig database;
    int logLevel;
};

namespace nlohmann {
    void from_json(const json& j, DatabaseConfig& cfg) {
        cfg.host = j.at("host").get<std::string>();
        cfg.port = j.value("port", 5432);
    }
    void from_json(const json& j, AppConfig& cfg) {
        cfg.database = j.at("database").get<DatabaseConfig>();
        cfg.logLevel = j.value("logLevel", 1);
    }
}

枚举映射:nlohmann 不内置枚举转换,需手动实现。推荐使用 std::unordered_map 双向查找:

cpp 复制代码
enum class QueryType { Face, Body, Photo };

const std::unordered_map<std::string, QueryType> kQueryTypeMap = {
    {"face", QueryType::Face},
    {"body", QueryType::Body},
    {"photo", QueryType::Photo}
};

namespace nlohmann {
    void from_json(const json& j, QueryType& t) {
        auto name = j.get<std::string>();
        auto it = kQueryTypeMap.find(name);
        if (it == kQueryTypeMap.end()) {
            throw std::invalid_argument("Unknown QueryType: " + name);
        }
        t = it->second;
    }
    void to_json(json& j, QueryType t) {
        for (const auto& [name, val] : kQueryTypeMap) {
            if (val == t) { j = name; return; }
        }
        j = nullptr;
    }
}

手动静态方法

静态 from_json 成员函数模式,将反序列化逻辑封装为类的工厂方法,优势在于:

  • 可传入上下文参数(如默认值、配置项)
  • 可在构造过程中执行业务校验与日志
  • 调用方显式选择反序列化入口,避免隐式转换
cpp 复制代码
class SearchQueryArgs {
public:
    int topN;
    int thresholdMin;
    int thresholdMax;
    std::string imgType;

    static SearchQueryArgs from_json(const nlohmann::json& inJson, int defTopN) {
        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) {
            throw EngineParamError("topN out of range");
        }
        return tmp;
    }
};

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++17 std::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 对象的并发读取在实践中安全,但标准未做保证。

相关推荐
Daydream.V17 小时前
C++ 入门全攻略:从基础语法到核心特性
java·开发语言·c++
我能坚持多久17 小时前
STL详解——stack以及queue的模拟实现
开发语言·c++·学习
无限进步_17 小时前
【C++】智能指针的设计逻辑:RAII与资源安全
c++·算法·安全
会周易的程序员17 小时前
AI 编程助手:从“猫弄乱的线团”到“击鼓传花”的 Bug 修复
c++·人工智能·物联网·架构·bug·iot
江屿风17 小时前
C++OJ题经验总结(竞赛)2
开发语言·c++·笔记·算法
W.W.H.17 小时前
C++ 设计模式:6 个常用模式的实战示例
开发语言·c++·设计模式
阿文的代码库17 小时前
C++的单例模式及其作用
开发语言·c++·单例模式
Shan120517 小时前
一文读懂:C++中单例模式的实现
java·c++·单例模式