第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
集成方式有两种:FetchContent 和 find_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,它会直接抛异常。
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::parse() 的第二个参数指定一个回调函数,实现SAX风格的流式解析,减少内存占用。
65.8 知识体系总览
下面这张图总结了nlohmann/json的核心知识结构,你可以把它当作速查表:
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