第65章:nlohmann/json——集成JSON解析库

JSON,说白了就是现代C++程序员的“通用语言”。配置文件、网络通信、数据交换……哪哪都离不开它。而nlohmann/json这个库,是我个人在C++项目里用得最顺手的JSON解析库,没有之一。

为什么这么说?因为它的设计哲学就一个字:。你不需要写一堆繁琐的解析代码,不需要关心内存管理,甚至不需要学习什么复杂的API——它用起来就像操作一个动态类型的Python字典。

好,咱们直接进入正题。

65.1 为什么是nlohmann/json?

市面上C++的JSON库不少,比如RapidJSON、jsoncpp、Boost.JSON。但我个人偏爱nlohmann/json,原因有三:

  • 头文件库:只有一个头文件,include进来就能用,省去一堆链接烦恼
  • 语法自然:用起来就像操作STL容器,[]运算符、迭代器、范围for循环,全都有
  • 异常安全:解析失败会抛异常,不会让你在野指针里打转

我记得有一次接手一个遗留项目,里面用的是某个老旧的JSON库,光编译就折腾了半天。后来我直接换成nlohmann/json,整个团队都松了一口气。

65.2 在CMake中集成nlohmann/json

集成方式有两种:FetchContentfind_package。我个人更推荐FetchContent,尤其是你希望项目“开箱即用”的时候。

方式一:FetchContent(推荐)

cmake_minimum_required(VERSION 3.14)
project(JsonDemo)

include(FetchContent)
FetchContent_Declare(
    nlohmann_json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(nlohmann_json)

add_executable(demo main.cpp)
target_link_libraries(demo PRIVATE nlohmann_json::nlohmann_json)

嗯,这里要注意:FetchContent_MakeAvailable 需要CMake 3.14以上版本。如果你还在用老版本,赶紧升级吧。

方式二:find_package(系统已安装)

find_package(nlohmann_json REQUIRED)
target_link_libraries(demo PRIVATE nlohmann_json::nlohmann_json)

这种方式适合你确定目标机器上已经安装了该库。但说实话,我在跨平台项目里吃过亏——有的机器没装,编译直接挂掉。所以我现在基本只用FetchContent。

65.3 核心用法速览

咱们直接看代码。这是nlohmann/json最常用的几个操作:

#include <nlohmann/json.hpp>
using json = nlohmann::json;

// 1. 从字符串解析
std::string raw = R"({"name":"张三","age":28,"skills":["C++","Python"]})";
json j = json::parse(raw);

// 2. 访问数据
std::string name = j["name"];
int age = j["age"];
std::string first_skill = j["skills"][0];

// 3. 修改数据
j["age"] = 29;
j["skills"].push_back("Go");

// 4. 序列化回字符串
std::string output = j.dump(4);  // 带缩进,好看
std::cout << output << std::endl;

你看,是不是很直观?[]运算符直接访问,push_back追加数组元素,dump输出字符串。我当初第一次用的时候,几乎没看文档就上手了。

65.4 从文件读取和写入

实际项目中,JSON数据通常来自文件。nlohmann/json对文件操作的支持也很到位:

#include <fstream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

// 从文件读取
std::ifstream fin("config.json");
json config;
fin >> config;

// 写入文件
std::ofstream fout("output.json");
fout << config.dump(2);

这里有个小坑:operator>>operator<< 是nlohmann/json重载的流运算符,用起来确实方便。但如果你文件路径不对,或者文件内容不是合法JSON,它会直接抛异常。

我曾经踩过的坑:有一次生产环境的配置文件里多了一个逗号,结果程序启动就崩。后来我加了一层try-catch,把解析失败的错误信息打印出来,问题才定位到。所以,解析JSON一定要加异常处理

65.5 复杂数据结构处理

JSON里经常有嵌套对象和数组。nlohmann/json处理起来也很顺手:

json complex = {
    {"user", {
        {"name", "李四"},
        {"address", {
            {"city", "北京"},
            {"district", "海淀"}
        }},
        {"hobbies", {"读书", "跑步", "编程"}}
    }},
    {"meta", {
        {"version", 2},
        {"timestamp", 1700000000}
    }}
};

// 访问嵌套数据
std::string city = complex["user"]["address"]["city"];
std::string hobby = complex["user"]["hobbies"][1];

// 遍历数组
for (auto& h : complex["user"]["hobbies"]) {
    std::cout << h << std::endl;
}

// 遍历对象
for (auto& [key, val] : complex["user"]["address"].items()) {
    std::cout << key << ": " << val << std::endl;
}

你想想看,如果用C++原生的方式去解析这种嵌套结构,得写多少代码?而nlohmann/json只需要几行。

65.6 类型检查与转换

JSON是动态类型的,但C++是静态类型的。所以从JSON取值时,类型要匹配。nlohmann/json提供了丰富的类型检查方法:

方法 说明
is_null() 是否为null
is_boolean() 是否为布尔值
is_number() 是否为数字
is_string() 是否为字符串
is_array() 是否为数组
is_object() 是否为对象
if (j["age"].is_number()) {
    int age = j["age"];
    // 安全转换
}

if (j["name"].is_string()) {
    std::string name = j["name"];
}

我个人习惯:在解析不可信的JSON数据时,先做类型检查,再取值。这样能避免很多运行时错误。

65.7 性能与注意事项

nlohmann/json的性能在大多数场景下完全够用。但如果你需要解析几百MB的超大JSON文件,或者对延迟有极致要求,那它可能不是最优选择——RapidJSON在这方面更胜一筹。

不过话说回来,我做了这么多年项目,99%的场景nlohmann/json都扛得住。只有一次,在嵌入式设备上解析一个50MB的配置文件,确实感觉有点慢。后来改成流式解析才解决。

小技巧:如果你只需要读取JSON中的某几个字段,可以用 json::parse() 的第二个参数指定一个回调函数,实现SAX风格的流式解析,减少内存占用。

65.8 知识体系总览

下面这张图总结了nlohmann/json的核心知识结构,你可以把它当作速查表:

nlohmann/json 集成方式 • FetchContent(推荐) • find_package • 直接拷贝头文件 核心操作 • parse() 解析字符串 • [] 访问/修改数据 • dump() 序列化 • 流式读写文件 类型系统 • is_null/is_boolean • is_number/is_string • is_array/is_object • 类型安全转换 高级特性 • SAX流式解析 • JSON Schema验证 • 自定义类型转换 避坑指南 • 解析必须加try-catch • 大文件注意性能 • 类型检查不可少

65.9 实战:配置文件解析

最后,咱们来个完整的实战例子。假设你有一个配置文件 app_config.json

{
    "server": {
        "host": "0.0.0.0",
        "port": 8080,
        "workers": 4
    },
    "database": {
        "type": "mysql",
        "host": "localhost",
        "port": 3306,
        "name": "myapp"
    },
    "logging": {
        "level": "info",
        "file": "/var/log/myapp.log"
    }
}

用nlohmann/json解析它:

#include <iostream>
#include <fstream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

struct AppConfig {
    std::string server_host;
    int server_port;
    int workers;
    std::string db_type;
    std::string db_host;
    int db_port;
    std::string db_name;
    std::string log_level;
    std::string log_file;
};

AppConfig load_config(const std::string& path) {
    std::ifstream fin(path);
    if (!fin.is_open()) {
        throw std::runtime_error("无法打开配置文件: " + path);
    }
    
    json j;
    fin >> j;
    
    AppConfig cfg;
    cfg.server_host = j["server"]["host"];
    cfg.server_port = j["server"]["port"];
    cfg.workers = j["server"]["workers"];
    cfg.db_type = j["database"]["type"];
    cfg.db_host = j["database"]["host"];
    cfg.db_port = j["database"]["port"];
    cfg.db_name = j["database"]["name"];
    cfg.log_level = j["logging"]["level"];
    cfg.log_file = j["logging"]["file"];
    
    return cfg;
}

int main() {
    try {
        auto cfg = load_config("app_config.json");
        std::cout << "服务器端口: " << cfg.server_port << std::endl;
        std::cout << "数据库类型: " << cfg.db_type << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "配置加载失败: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

这个例子涵盖了文件读取、嵌套访问、异常处理等核心知识点。你在实际项目中可以直接套用这个模板。

核心要点回顾:

  • 用FetchContent集成,省心省力
  • 解析JSON一定要加try-catch
  • 类型检查是防御性编程的好习惯
  • nlohmann/json适合95%的场景,超大文件考虑RapidJSON

好了,这一章就到这里。nlohmann/json是个让人用着很舒服的库,希望你在项目里也能体会到这种“丝滑”的感觉。


公众号:蓝海资料掘金营,微信deep3321