跳转至

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 值继续访问是未定义行为

评论