C++ 处理 JSON:nlohmann/json 实战
处理 JSON 是现代 C++ 服务的日常。事实上的标准库是 nlohmann/json:单头文件、直觉语法(
json对象用起来像 STL 容器)、活跃维护。本篇工程在 WSL cmake 3.27 + g++ 11 通过 FetchContent 构建运行验证,接入方式见 CMake 工程实战。
接入
include(FetchContent)
FetchContent_Declare(json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
)
FetchContent_MakeAvailable(json)
target_link_libraries(app PRIVATE nlohmann_json::nlohmann_json)
解析:字符串与文件
#include <nlohmann/json.hpp>
#include <fstream>
#include <cstdio>
using json = nlohmann::json;
int main() {
// 从字符串解析
json j = json::parse(R"(
{
"name": "ada",
"score": 92.5,
"tags": ["cpp", "dev"],
"active": true,
"profile": { "city": "shanghai" }
}
)");
// 读值:operator[] 像容器一样用
std::string name = j["name"];
double score = j["score"];
bool active = j["active"];
std::string city = j["profile"]["city"]; // 嵌套访问
std::printf("%s %.1f %d %s\n", name.c_str(), score, (int)active, city.c_str());
// 从文件解析
std::ifstream("config.json") >> j;
}
类型转换是显式的:赋给 std::string/double/int 时自动按存储类型转换;类型不符会抛 type_error。读取前可用 is_string() / is_number() / contains("key") 预检查。
序列化:构造与输出
json out;
out["name"] = "bob";
out["scores"] = {90, 85, 77}; // 列表
out["profile"] = {{"city", "beijing"}, {"age", 30}}; // 嵌套对象
std::string compact = out.dump(); // 紧凑单行
std::string pretty = out.dump(2); // 缩进 2 的格式化文本
std::printf("%s\n", pretty.c_str());
// 写文件
std::ofstream("out.json") << out.dump(2);
{{"k", v}, ...} 的初始化列表语法让"手写 JSON 对象"几乎和 Python 字典一样自然。
遍历与修改
// 遍历对象
for (auto& [key, value] : j.items())
std::printf("%s = %s\n", key.c_str(), value.dump().c_str());
// 遍历数组
for (auto& tag : j["tags"])
std::printf("%s\n", tag.get<std::string>().c_str());
// 修改与增删
j["score"] = 99.0;
j["extra"] = "new field";
j.erase("active");
自定义类型:一行宏互转
struct User {
std::string name;
int age;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(User, name, age) // 非侵入:不改动类定义
User u{"ada", 30};
json ju = u; // 自动序列化
User back = ju.get<User>(); // 自动反序列化
宏生成 to_json/from_json;字段名与成员名一致。需要校验/默认值的场景手写 from_json 并抛 json::exception。
错误处理
// 解析失败抛 parse_error
try {
json::parse("{ broken }");
} catch (const json::parse_error& e) {
std::printf("parse error at byte %zu: %s\n", e.byte, e.what());
}
// 不抛异常的版本:失败返回 discarded 值
json j = json::parse("{ broken }", nullptr, false);
if (j.is_discarded()) {
std::printf("invalid json\n");
}
服务代码处理不可信输入时,用 is_discarded 版本 + 显式分支比异常更可控(取舍见错误处理)。
实战:读配置、写报告
#include <nlohmann/json.hpp>
#include <fstream>
#include <cstdio>
int main() {
// 读配置(带默认值的健壮写法)
json cfg = json::parse(std::ifstream("config.json"));
int port = cfg.value("port", 8080); // 不存在用默认值
std::string host = cfg.value("host", std::string("0.0.0.0"));
// 写报告
json report;
report["host"] = host;
report["port"] = port;
report["generated"] = std::time(nullptr);
std::ofstream("report.json") << report.dump(2);
std::printf("report: %s:%d\n", host.c_str(), port);
}
value(key, default) 是读取可选字段的标配——不存在时给默认值而不是抛异常。
常见错误
| 错误 | 说明 |
|---|---|
| 访问不存在的键 | j["missing"] 对 const 对象抛异常;非 const 会插入 null!只读场景用 contains / value |
| 类型不匹配赋值 | int 里存的是 "42"(字符串)时 get<int>() 抛 type_error;先 is_number_integer() 判断 |
| 超大整数 | 默认整型是 64 位(number_integer_t),超过再考虑 number_unsigned_t |
| 浮点精度 | dump 默认保留足够精度,但 round-trip 前后可能有末位差异;金融数值用字符串传递 |
| 巨型文件 | DOM 模式整棵载入内存;几百 MB 以上改用 SAX 接口流式处理 |
忘记处理 is_discarded |
parse(..., false) 失败不抛异常,但拿 discarded 值继续访问是未定义行为 |