nlohmann/json(2)
本文展示了如何使用 nlohmann/json 库高效处理复杂 JSON 数据,包括: 安全解析嵌套数据和处理可选字段(使用.value()方法提供默认值) 遍历JSON数组和防御性编程(使用.contains()检查键是否存在) JSON与二进制格式(如MessagePack)的互转,节省存储空间 关键点: 使用_json后缀快速创建JSON对象 通过.value("key"
在实际开发中(例如处理 HTTP API 响应),JSON 数据往往比较复杂:字段可能缺失(Optional)、结构可能嵌套多层。
这个例子展示了如何安全地解析复杂的嵌套数据,处理可选字段(使用默认值),以及如何遍历数组。
实战场景:解析用户列表响应
假设你收到了服务器发来的 JSON,其中包含一个用户列表。有些用户没有 email 字段,我们需要优雅地处理这种情况。
#include <iostream>
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
using json = nlohmann::json;
int main() {
// 模拟从服务器收到的 JSON 数据
// 注意:第二个用户 "Guest" 缺少 "email" 字段
auto api_response = R"(
{
"status": 200,
"message": "success",
"data": {
"total_count": 2,
"users": [
{
"id": 101,
"name": "Admin User",
"email": "admin@sys.com",
"features": ["audit", "delete", "write"]
},
{
"id": 102,
"name": "Guest User",
// "email" 字段缺失
"features": ["read_only"]
}
]
}
}
)"_json; // _json 后缀直接将字面量转为 json 对象
// 1. 检查业务状态码 (使用 .value() 防止 key 不存在报错)
// .value("key", default_value) -> 如果 key 存在则返回对应值,否则返回默认值
int status = api_response.value("status", -1);
if (status != 200) {
std::cerr << "API Error!" << std::endl;
return 1;
}
// 2. 安全地进入嵌套层级
// 检查 "data" 里的 "users" 是否存在且是数组
if (api_response.contains("data") && api_response["data"]["users"].is_array()) {
std::cout << "--- User List ---" << std::endl;
// 3. 遍历数组 (使用引用避免拷贝)
for (const auto& user : api_response["data"]["users"]) {
// 获取必须存在的字段
int id = user["id"];
std::string name = user["name"];
// 4. 处理可选字段 (Optional Field)
// 如果 JSON 里没有 "email",我们使用 "N/A" 作为默认值
std::string email = user.value("email", "N/A");
std::cout << "[ID: " << id << "] " << name << " | Email: " << email << std::endl;
// 5. 处理嵌套的数组 (Features)
if (user.contains("features")) {
std::cout << " Permissions: ";
for (const auto& feature : user["features"]) {
std::cout << feature << " ";
}
std::cout << std::endl;
}
}
}
return 0;
}
代码关键点解析
_json后缀:R"(...)"_json是库提供的字面量操作符,直接将字符串变成了 json 对象,省去了json::parse()的步骤,写测试代码时非常方便。.value("key", default):
这是最重要的技巧之一。相比于j["key"](如果 key 不存在会抛出异常或未定义行为),value()方法允许你提供一个默认值。- 例子中用于处理缺失的 Email 字段,防止程序崩溃。
.contains("key"):
在访问之前先判断键是否存在,这在处理不受信任的外部数据时是必须的防御性编程。- 嵌套访问:
你可以像链式调用一样直接深入访问:api_response["data"]["users"]。
二进制格式互转。
在网络传输或存储时,普通的 JSON 字符串体积较大且解析慢。这个库支持将 JSON 直接转换为 MessagePack、CBOR 或 BSON 等二进制格式。
实战场景:高效传输 (JSON vs MessagePack)
这个例子展示如何将一个 JSON 对象压缩成 MessagePack 二进制数据(体积更小),然后再把它还原回来。
#include <iostream>
#include <nlohmann/json.hpp>
#include <vector>
#include <iomanip> // 用于输出 hex
using json = nlohmann::json;
int main() {
// 1. 创建一个包含混合数据的 JSON
json j;
j["id"] = 1001;
j["compact"] = true;
j["pi"] = 3.1415926535; // 浮点数在纯文本 JSON 中占很多字节
// 模拟一些二进制数据 (比如图片像素点)
j["data"] = {255, 0, 128, 64, 32};
// --- 转换对比 ---
// 方式 A: 标准 JSON 字符串 (Text)
std::string json_str = j.dump();
std::cout << "JSON 文本大小: " << json_str.size() << " bytes" << std::endl;
std::cout << "内容: " << json_str << "\n" << std::endl;
// 方式 B: MessagePack 二进制 (Binary)
// to_msgpack 返回 std::vector<uint8_t>
std::vector<uint8_t> msgpack_data = json::to_msgpack(j);
std::cout << "MessagePack 大小: " << msgpack_data.size() << " bytes" << std::endl;
std::cout << "二进制数据(Hex): ";
for (auto byte : msgpack_data) {
// 打印十六进制查看
std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)byte << " ";
}
std::cout << std::dec << "\n" << std::endl;
// --- 还原数据 (反序列化) ---
// 从二进制数据还原回 json 对象
json j_restored = json::from_msgpack(msgpack_data);
// 验证数据
if (j_restored["id"] == 1001 && j_restored["pi"] == 3.1415926535) {
std::cout << "成功还原: " << j_restored["id"] << std::endl;
}
return 0;
}
这个例子的核心价值
- 节省空间:
- JSON 文本中的
3.1415926535需要 12 个字节(每个字符 1 字节)。 - 在 MessagePack 二进制中,它只是一个
double类型,通常占用 8 字节(甚至更少,取决于压缩策略)。 - 对于包含大量数字数组的数据,二进制格式通常能节省 30%-50% 的空间。
- JSON 文本中的
- 无缝切换:
- 你不需要为了二进制协议去学新的库(比如 Protobuf)。在调试阶段用 JSON (
dump()) 看文本,在发布阶段用to_msgpack()提升性能,代码结构完全不用变。
- 你不需要为了二进制协议去学新的库(比如 Protobuf)。在调试阶段用 JSON (
- 支持多种格式:
- 除了
to_msgpack,你也可用to_cbor(Concise Binary Object Representation) 或to_bson(MongoDB 使用的格式)。用法完全一样。
- 除了
快速总结:该库的 “三板斧”
回顾一下我们这三个例子涵盖的内容,这是掌握 nlohmann/json 的核心路径:
- 基础用法:像用
std::map一样赋值,用.dump()输出。 - 防御性解析:用
.value()处理缺失字段,用.get<Type>()转换类型。 - 高级转换:用
to_msgpack处理二进制,或用宏NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE绑定结构体。
好的,这最后一个例子我们来介绍一个非常高级且实用的功能:JSON Diff 和 Patch (比较与打补丁)。
在嵌入式开发或后台系统中,我们经常需要比较两个配置文件的差异,或者只发送变更的部分(Delta Update)来节省带宽。
nlohmann/json 完全支持 RFC 6902 标准,可以自动计算两个 JSON 对象的差异,并生成补丁。
实战场景:无线模块配置更新 (Configuration Delta)
假设你的设备有一个“当前配置”,服务器下发了一个“新配置”。你想知道变了什么,并把变化应用上去。
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main() {
// 1. 原始配置 (比如设备当前的参数)
json current_config = R"(
{
"module_name": "Qualcomm_X55",
"frequency": 2400,
"power_saving": true,
"settings": {
"retries": 3,
"timeout_ms": 1000
}
}
)"_json;
// 2. 目标配置 (我们希望变成的样子)
// 注意:
// - "frequency" 变了 (2400 -> 5000)
// - "power_saving" 变了 (true -> false)
// - "settings" 里新增了 "mode": "turbo"
// - "module_name" 没变
json target_config = R"(
{
"module_name": "Qualcomm_X55",
"frequency": 5000,
"power_saving": false,
"settings": {
"retries": 3,
"timeout_ms": 1000,
"mode": "turbo"
}
}
)"_json;
// 3. 计算差异 (Generate Diff)
// diff() 函数会返回一个包含所有变更操作的 JSON 数组 (遵循 RFC 6902)
json patch = json::diff(current_config, target_config);
std::cout << "--- 变更补丁 (Diff) ---" << std::endl;
std::cout << patch.dump(4) << std::endl;
// 4. 应用补丁 (Apply Patch)
// 假设我们在另一端只收到了 patch,我们可以将其应用到当前配置上
json applied_config = current_config.patch(patch);
std::cout << "\n--- 应用补丁后的新配置 ---" << std::endl;
std::cout << applied_config.dump(4) << std::endl;
// 5. 验证是否一致
if (applied_config == target_config) {
std::cout << "\n✅ 配置更新成功!" << std::endl;
}
return 0;
}
输出结果详解
看看 diff 生成的输出,非常有意思,它精确描述了如何从 A 变到 B:
[
{
"op": "replace",
"path": "/frequency",
"value": 5000
},
{
"op": "replace",
"path": "/power_saving",
"value": false
},
{
"op": "add",
"path": "/settings/mode",
"value": "turbo"
}
]
为什么这个功能很棒?
- 极小的传输量:
你不需要发送完整的target_config。如果配置很大但只改了一个参数,你只需要发送上面那个小小的patch数组。这对于OTA (Over-the-Air) 更新或带宽受限的无线模块非常有用。 - 审计日志 (Audit Logs):
你可以把patch结果存入日志数据库,这样你就能精确记录:“用户在 12:00 将 frequency 从 2400 修改为了 5000”。 - 标准化:
生成的格式遵循 IETF RFC 6902,这意味着你的 C++ 后端生成的 diff,前端的 JavaScript 或 Python 脚本也能直接看懂并应用。
这是一个非常基础但极其重要的例子:手动处理字段映射 (Manual Mapping)。
之前的例子中我们使用了宏 (NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE),它要求 JSON 的 key 必须和 C++ 的变量名完全一致。
但在现实世界(特别是嵌入式 Linux 与 Web 后端交互时),命名风格往往不一致:
- C++ (Linux/Driver 风格): 常用下划线命名 (
device_id,battery_level)。 - JSON (Web/Java 风格): 常用驼峰命名 (
deviceId,batteryLevel)。
这个例子展示如何手动编写转换函数来解决名字不匹配的问题,这也是该库最灵活的用法。
实战场景:适配不同命名风格的 API
假设你的嵌入式设备代码遵循 Linux 规范(下划线),但服务器下发的 JSON 是驼峰命名的。
#include <iostream>
#include <nlohmann/json.hpp>
#include <string>
using json = nlohmann::json;
// 1. 定义你的 C++ 结构体 (Linux 风格: snake_case)
struct WirelessModule {
int chip_id;
std::string firmware_ver;
bool is_active;
};
// 2. 手动定义 to_json 函数
// 作用:C++ -> JSON (序列化)
// 注意:函数名必须是 to_json,且与结构体在同一个命名空间下
void to_json(json& j, const WirelessModule& m) {
j = json{
// 在这里手动指定 JSON 的 Key 名称
{"chipId", m.chip_id},
{"firmwareVersion", m.firmware_ver}, // 甚至可以改名:firmware_ver -> firmwareVersion
{"isActive", m.is_active}
};
}
// 3. 手动定义 from_json 函数
// 作用:JSON -> C++ (反序列化)
void from_json(const json& j, WirelessModule& m) {
// j.at("key") 会检查 key 是否存在,不存在则抛出异常,比 j["key"] 更安全
j.at("chipId").get_to(m.chip_id);
j.at("firmwareVersion").get_to(m.firmware_ver);
j.at("isActive").get_to(m.is_active);
}
int main() {
// --- 场景 1: 解析服务器下发的 JSON (CamelCase) ---
json api_response = R"(
{
"chipId": 865,
"firmwareVersion": "1.0.4-stable",
"isActive": true
}
)"_json;
// 库会自动查找并调用我们写的 from_json 函数
WirelessModule module = api_response.get<WirelessModule>();
std::cout << "--- C++ Object ---" << std::endl;
std::cout << "Chip ID: " << module.chip_id << std::endl; // 访问 C++ 变量
std::cout << "Ver: " << module.firmware_ver << std::endl;
// --- 场景 2: 上传数据给服务器 (转回 CamelCase) ---
module.is_active = false; // 修改状态
// 库会自动查找并调用我们写的 to_json 函数
json upload_data = module;
std::cout << "\n--- JSON for Upload ---" << std::endl;
std::cout << upload_data.dump(4) << std::endl;
// 输出的 JSON key 将会自动变回 "chipId" 等驼峰形式
return 0;
}
代码核心点
- ADL (Argument-Dependent Lookup):
你不需要显式调用to_json或from_json。只要这两个函数和你的结构体WirelessModule定义在同一个namespace下,当你执行j = module;或module = j;时,编译器会自动找到它们。 .get_to(variable):
这是from_json中最常用的写法。它等同于variable = j.at("key").get<Type>(),但是写起来更简洁。- 灵活性:
手动写这两个函数虽然比宏稍微麻烦一点,但它给了你无限的控制权。- 比如:JSON 里的
status: "OK"可以在from_json里直接被转成 C++ 的bool is_ok = true。 - 比如:你可以只解析 JSON 里的某几个字段,忽略其他的。
- 比如:JSON 里的
更多推荐


所有评论(0)