在实际开发中(例如处理 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;
}

代码关键点解析

  1. _json 后缀:
    R"(...)"_json 是库提供的字面量操作符,直接将字符串变成了 json 对象,省去了 json::parse() 的步骤,写测试代码时非常方便。
  2. .value("key", default):
    这是最重要的技巧之一。相比于 j["key"](如果 key 不存在会抛出异常或未定义行为),value() 方法允许你提供一个默认值。
    • 例子中用于处理缺失的 Email 字段,防止程序崩溃。
  3. .contains("key"):
    在访问之前先判断键是否存在,这在处理不受信任的外部数据时是必须的防御性编程。
  4. 嵌套访问:
    你可以像链式调用一样直接深入访问:api_response["data"]["users"]

二进制格式互转

在网络传输或存储时,普通的 JSON 字符串体积较大且解析慢。这个库支持将 JSON 直接转换为 MessagePackCBORBSON 等二进制格式。

实战场景:高效传输 (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;
}

这个例子的核心价值

  1. 节省空间
    • JSON 文本中的 3.1415926535 需要 12 个字节(每个字符 1 字节)。
    • 在 MessagePack 二进制中,它只是一个 double 类型,通常占用 8 字节(甚至更少,取决于压缩策略)。
    • 对于包含大量数字数组的数据,二进制格式通常能节省 30%-50% 的空间。
  2. 无缝切换
    • 你不需要为了二进制协议去学新的库(比如 Protobuf)。在调试阶段用 JSON (dump()) 看文本,在发布阶段用 to_msgpack() 提升性能,代码结构完全不用变。
  3. 支持多种格式
    • 除了 to_msgpack,你也可用 to_cbor (Concise Binary Object Representation) 或 to_bson (MongoDB 使用的格式)。用法完全一样。

快速总结:该库的 “三板斧”

回顾一下我们这三个例子涵盖的内容,这是掌握 nlohmann/json 的核心路径:

  1. 基础用法:像用 std::map 一样赋值,用 .dump() 输出。
  2. 防御性解析:用 .value() 处理缺失字段,用 .get<Type>() 转换类型。
  3. 高级转换:用 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"
    }
]

为什么这个功能很棒?

  1. 极小的传输量
    你不需要发送完整的 target_config。如果配置很大但只改了一个参数,你只需要发送上面那个小小的 patch 数组。这对于OTA (Over-the-Air) 更新带宽受限的无线模块非常有用。
  2. 审计日志 (Audit Logs)
    你可以把 patch 结果存入日志数据库,这样你就能精确记录:“用户在 12:00 将 frequency 从 2400 修改为了 5000”。
  3. 标准化:
    生成的格式遵循 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;
}

代码核心点

  1. ADL (Argument-Dependent Lookup):
    你不需要显式调用 to_jsonfrom_json。只要这两个函数和你的结构体 WirelessModule 定义在同一个 namespace 下,当你执行 j = module;module = j; 时,编译器会自动找到它们。
  2. .get_to(variable):
    这是 from_json 中最常用的写法。它等同于 variable = j.at("key").get<Type>(),但是写起来更简洁。
  3. 灵活性:
    手动写这两个函数虽然比宏稍微麻烦一点,但它给了你无限的控制权
    • 比如:JSON 里的 status: "OK" 可以在 from_json 里直接被转成 C++ 的 bool is_ok = true
    • 比如:你可以只解析 JSON 里的某几个字段,忽略其他的。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐