当前位置: 代码网 > it编程>编程语言>Javascript > nlohmann/json 深度解析之如何让 C++ 解析 JSON 像喝水一样简单

nlohmann/json 深度解析之如何让 C++ 解析 JSON 像喝水一样简单

2026年08月07日 Javascript 我要评论
一、nlohmann/json 是什么?一句话:nlohmann/json(官方名 json for modern c++)是 c++ 生态里最"亲民"的开源 json 解析库,由

一、nlohmann/json 是什么?

一句话:nlohmann/json(官方名 json for modern c++)是 c++ 生态里最"亲民"的开源 json 解析库,由德国开发者 niels lohmann 维护,github 上 star 数接近 5 万,是目前 c++ 社区使用最广泛的 json 库之一。

打个比方:别的 json 库像是"手工拧螺丝"——你得自己管理内存、自己写遍历逻辑;nlohmann/json 则像"电动螺丝刀"——你只要说"我要读这个字段",它就把活干完了。

它最大的特点是 header-only(单头文件):整个库只有一个 json.hpp(约 2.5 万行),#include 进去就能用,不需要链接任何 .lib / .so,不需要安装额外依赖,不需要配置 cmake find_package 的烦恼

// 全库只需要这一行引入,就是这么简单
#include <nlohmann/json.hpp>
using nlohmann::json;   // 取个短名字,后面写起来省事

二、使用优点:为什么大家都在用它?

2.1 header-only 单头文件,零配置零依赖

这是它碾压传统方案的第一大优势。你只需要把 json.hpp 拷进项目,或者用 cmake 的 fetchcontent / 包管理器(vcpkg、conan)拉下来即可。

// cmake 三种最常用的引入方式(任选其一)
// 方式一:fetchcontent(推荐,自动下载)
// include(fetchcontent)
// fetchcontent_declare(nlohmann_json url https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz)
// fetchcontent_makeavailable(nlohmann_json)
// target_link_libraries(你的目标 private nlohmann_json::nlohmann_json)
// 方式二:vcpkg
// vcpkg install nlohmann-json
// 方式三:把 json.hpp 直接放进 include 目录
// #include <nlohmann/json.hpp> 即可

⚠️ 预警:虽然叫"单头文件",但 json.hpp 有 2.5 万行、编译较慢。如果项目里多个 .cpp 都 include 它,建议只在少数几个"门面"文件里 include,或者用 -fvisibility 等手段控制,否则会增加编译时间。

2.2 类型安全:像用 python 一样写 c++ json

传统 c 风格解析(如 cjson)需要你手动判断类型、手动释放内存,一个 free 忘了就内存泄漏。nlohmann/json 底层封装了 std::variant 语义,自动管理生命周期,json 的值在析构时自动回收

更妙的是,它提供了 is_xxx() 系列类型判断函数,以及 .get<t>() / .as<t>() 类型转换,类型错了会抛异常而不是静默返回垃圾值

2.3 与 stl 容器天然互通

这是它区别于很多 json 库的杀手锏:json 对象可以直接和 std::map、std::vector、std::string、std::optional 互相转换,不需要写任何胶水代码。你甚至可以直接把整个 json 对象赋给 std::vector<int>。

std::vector<int> nums = {1, 2, 3};
json j = nums;              // 容器 -> json,一行搞定
std::vector<int> back = j;  // json -> 容器,还是返回 std::vector<int>

2.4 现代 c++ 特性全家桶

  • 支持 c++11 起的所有标准(c++11/14/17/20/23 都兼容);
  • 支持 初始化列表 直接构造 json,写起来像 python 字典;
  • 支持 范围 for 循环 遍历(for (auto& [key, val] : j.items()));
  • 支持 结构化绑定移动语义
  • 提供 std::optional / std::variant 的适配,错误处理现代化。

2.5 错误处理友好

解析失败、类型不匹配、键不存在时,都会抛出带详细位置的异常(json::parse_error 会告诉你出错在第几行第几列),而不是静默失败。

try {
    auto j = json::parse(r"({"a": 1, )");  // 故意写个语法错误
} catch (const json::parse_error& e) {
    std::cout << "解析失败: " << e.what() << std::endl;
    // 输出会包含 byte 位置,方便定位
}

三、使用场景:它在真实世界都干了什么活?

场景典型例子用 nlohmann/json 的姿势
配置文件解析软件的 config.json(数据库地址、端口、开关项)json::parse(ifstream) 读进来,j.at("port").get<int>() 取参数
网络 api 数据交换调用 restful api,收发 json 报文请求体用 j.dump() 序列化;响应体用 json::parse() 反序列化
序列化 / 反序列化把 c++ 对象存成 json 落盘,或从 json 恢复对象自定义类实现 to_json / from_json 两个函数即可"自动"序列化
数据库交互把查询结果转 json 返回给前端(常配合 postgresql/mongodb 的 json 字段)结果集行 -> json 数组 -> dump()
前后端通信websocket / http 消息传递、日志结构化输出结构化日志直接 json 拼装后 dump() 写文件
测试数据构造单元测试里构造各种嵌套 json 输入初始化列表一行搞定,比手拼字符串可读性高一个量级

3.1 场景示例:读配置文件

#include <nlohmann/json.hpp>
#include <fstream>
#include <iostream>
using nlohmann::json;
int main() {
    // 假设 config.json 内容: {"server": {"host": "127.0.0.1", "port": 8080}, "debug": true}
    std::ifstream f("config.json");
    json cfg = json::parse(f);   // 直接吃流对象,不用先读成字符串
    std::string host = cfg["server"]["host"].get<std::string>();
    int port = cfg["server"]["port"].get<int>();
    bool debug = cfg["debug"].get<bool>();
    std::cout << "连接 " << host << ":" << port
              << (debug ? " (调试模式)" : "") << std::endl;
    return 0;
}

3.2 场景示例:调用 http api 收发 json

// 伪代码示意:真实网络请求请用 libcurl / httplib 等
json request;
request["action"] = "login";
request["user"] = "alice";
request["tags"] = {"cpp", "json"};       // 数组直接塞
// 序列化发送
std::string body = request.dump();       // {"action":"login","user":"alice","tags":["cpp","json"]}
// 假设收到响应字符串,反序列化
std::string response_str = r"({"code":0,"data":{"token":"abc123","expire":3600}})";
json resp = json::parse(response_str);
if (resp.at("code").get<int>() == 0) {
    auto token = resp["data"]["token"].get<std::string>();
    std::cout << "登录成功, token=" << token << std::endl;
}

四、具体使用方式:从安装到实战

4.1 安装(三步走,小白友好)

方式 a:直接拷贝(最快)

  1. 打开 nlohmann/json github releases(或直接去官方下载页);
  2. 下载最新版(如 v3.11.3)的 json.hpp;
  3. 把它放进项目的 include/nlohmann/ 目录下,然后:
#include <nlohmann/json.hpp>   // 结束,真的就这么简单

方式 b:vcpkg(windows 推荐)

vcpkg install nlohmann-json
# 然后在 cmakelists.txt 里:
# find_package(nlohmann_json config required)
# target_link_libraries(你的目标 private nlohmann_json::nlohmann_json)

方式 c:cmake fetchcontent(跨平台推荐)

include(fetchcontent)
fetchcontent_declare(nlohmann_json
    url https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz)
fetchcontent_makeavailable(nlohmann_json)

安装后验证:写一个 3 行的 hello 程序,编译运行不报错,就算装好了。

#include <nlohmann/json.hpp>
#include <iostream>
using nlohmann::json;
int main() {
    json j = {{"hello", "world"}};
    std::cout << j.dump() << std::endl;   // 输出 {"hello":"world"}
    return 0;
}
// 编译(g++): g++ -std=c++17 main.cpp -o main
// 编译(msvc): cl /std:c++17 /ehsc main.cpp

⚠️ 预警:库要求至少 c++11。用 msvc 编译务必加 /ehsc(异常处理开关),否则 catch 可能失效;用 gcc/clang 建议至少 -std=c++17 以获得结构化绑定等更现代体验。

4.2 解析 json(parse)

核心 api 就三个:json::parse(字符串)、json::parse(流)、json::parse(迭代器)。

// 1. 从字符串解析
auto j1 = json::parse(r"({"name": "alice", "age": 30})");
// 2. 从文件流解析
std::ifstream fin("data.json");
auto j2 = json::parse(fin);
// 3. 从 c 字符串指针 + 长度解析(跳过前 5 个字节的场景)
const char* raw = "xxxxx{\"k\": 1}";
auto j3 = json::parse(raw + 5, raw + 13);   // 传入起止迭代器,只解析 {"k": 1}
// 4. 宽容模式:允许注释、尾随逗号(对人工手写的配置非常友好)
auto j4 = json::parse(r"({
    "host": "localhost",   // 这是注释,标准 json 不允许
    "port": 8080,          // 尾逗号,标准 json 也不允许
})", nullptr, /*allow_exceptions=*/true, /*ignore_comments=*/true);
// 5. 更宽松:允许尾随逗号 + 非严格数字
auto j5 = json::parse("[1, 2, 3, ]", nullptr, true, true, /*ignore_trailing_comma=*/true);

⚠️ 预警:默认 parse 会严格拒绝注释和尾逗号。如果解析"人工维护的配置文件"报错,请检查是不是配置里写了注释——这种场景建议开启 ignore_comments = true。但网络传输的 json 请保持严格模式,不要图省事开宽松,否则等于放行走样数据。

值的类型判断与访问:

json v;
v = 42;                 // 现在是 number
std::cout << v.is_number() << std::endl;   // 1 (true)
v = "hello";            // 现在是 string
std::cout << v.is_string() << std::endl;   // 1 (true)
std::cout << v.is_null() << std::endl;     // 0
// 常用类型判断全家桶
// is_object()  is_array()  is_string()  is_number()  is_boolean()  is_null()
// is_number_integer()  is_number_unsigned()  is_number_float()

4.3 构建 json(build)

初始化列表语法是它最舒服的地方,没有之一:

json j;
j["name"] = "bob";              // 直接赋值,自动创建对象
j["age"] = 25;
j["skills"] = {"c++", "python", "sql"};            // 数组
j["address"]["city"] = "beijing";                  // 嵌套对象,自动创建中间层
// 更地道的写法:一条初始化列表全搞定
json profile = {
    {"name", "bob"},
    {"age", 25},
    {"skills", {"c++", "python", "sql"}},
    {"address", {{"city", "beijing"}, {"zip", "100000"}}},
    {"married", false},
    {"salary", nullptr}                            // null 也支持
};

⚠️ 预警(初始化列表的经典坑):{{"key", "value"}} 这种写法默认生成的是 object(对象),不是数组。想生成"包含一个对象的数组",必须写成 json::array({{"key","value"}}) 或 {{{...}}} 外层再包一层。

json wrong = {{"a", 1}};        // 这是 object: {"a":1}
json right = json::array({{"a", 1}});  // 这才是数组: [{"a":1}]

构建数组的另外两种姿势:

json arr = json::array();      // 空数组
arr.push_back(1);
arr.push_back(2);
arr.emplace_back("three");     // 就地构造,避免拷贝
// 或者直接数组初始化
json arr2 = {1, 2, 3, 4, 5};

二进制数据怎么放? json 没有二进制类型,惯例是 base64 编码成字符串,或者用 json::binary(该库提供扩展支持)。

std::vector<std::uint8_t> blob = {0x01, 0x02, 0xff};
json j;
j["data"] = json::binary(blob);           // 存成 binary 扩展
// 取回
auto bin = j["data"].get_binary();

4.4 遍历与修改(access & modify)

按 key 取值有三种姿势,推荐顺序也分三档:

json j = {{"name", "alice"}, {"age", 30}, {"hobby", {"reading", "swimming"}}};
// 姿势一:operator[] —— 最方便,但有两个坑!
auto name1 = j["name"];            // ✅ 能取到
auto none1 = j["salary"];          // ❌ 不存在时不会报错,而是【自动创建一个 null 成员】!
                                   //    也就是说 j 现在多了个 "salary": null
// 姿势二:.at() —— 安全,键不存在抛 out_of_range 异常
try {
    auto age = j.at("age");
} catch (const json::out_of_range& e) {
    std::cout << "键不存在: " << e.what() << std::endl;
}
// 姿势三:.find() —— 先查再取,不抛异常也不改结构
auto it = j.find("hobby");
if (it != j.end()) {
    auto hobby = *it;              // 找到了,取值
}

⚠️ 预警(新手必踩的坑):j["不存在的键"] 不会抛异常,而是会往 json 里新增一个 null 键!如果拿它做"只读探测",会意外污染数据。只读场景请用 .at() 或 .find()。

遍历所有成员(两种主流写法):

// 写法一:items() + 结构化绑定(c++17)
for (const auto& [key, value] : j.items()) {
    std::cout << "key=" << key << ", value=" << value << std::endl;
}
// 写法二:传统迭代器
for (auto it = j.begin(); it != j.end(); ++it) {
    std::cout << it.key() << " => " << it.value() << std::endl;
}
// 遍历数组
json arr = {10, 20, 30};
for (const auto& item : arr) {
    std::cout << item << std::endl;
}
// 带下标遍历数组(c++20 甚至可以直接用带下标的 range-for 语法糖)
for (auto [idx, item] : arr.items()) {      // items() 对数组也有效!
    std::cout << idx << ": " << item << std::endl;
}

修改与删除:

json j = {{"a", 1}, {"b", 2}, {"c", 3}};
j["b"] = 20;                // 修改:b 变成 20
j["d"] = 4;                 // 新增:d=4
j.erase("a");               // 删除:删掉键 a
j.clear();                  // 清空所有

合并(类似 python dict.update):

json base = {{"name", "tom"}, {"age", 18}};
json patch = {{"age", 19}, {"city", "shanghai"}};
base.update(patch);         // 递归合并:age 被覆盖为 19,city 被加入

4.5 与 std::vector / std::map 互转(stl interop)

这是 nlohmann/json 最吸引人的特性之一:json 与标准容器之间的转换是"免费"的

// vector <-> json 数组
std::vector<int> v = {1, 2, 3};
json jv = v;                            // [1,2,3]
auto v2 = jv.get<std::vector<int>>();   // 转回 vector
// map <-> json 对象(注意:key 必须是 string 类型)
std::map<std::string, int> m = {{"apple", 1}, {"banana", 2}};
json jm = m;                            // {"apple":1,"banana":2}
auto m2 = jm.get<std::map<std::string, int>>();
// 更复杂的嵌套容器也没问题
std::vector<std::map<std::string, double>> data = {{{"x", 1.5}, {"y", 2.5}}, {{"x", 3.5}}};
json jd = data;                         // 直接整棵转
auto back = jd.get<decltype(data)>();   // 再整棵转回来

⚠️ 预警:get<t>() 转换失败会抛 json::type_error。比如把字符串 "123" 用 get<int>() 取,会抛异常——它不会帮你做字符串转数字的隐式转换

json j = "123";    // 注意这是字符串
try {
    int n = j.get<int>();   // 抛 type_error!字符串不会自动转数字
} catch (const json::type_error& e) {
    std::cout << "类型不匹配: " << e.what() << std::endl;
}
// 正确姿势:先转 string 再手动 std::stoi
int n = std::stoi(j.get<std::string>());

自定义类型的序列化(to_json / from_json):

想让自己的类也能 json j = myobj,只要写两个函数(或者特化 adl_serializer):

struct point {
    int x, y;
};
// 序列化:对象 -> json
void to_json(json& j, const point& p) {
    j = json{{"x", p.x}, {"y", p.y}};
}
// 反序列化:json -> 对象
void from_json(const json& j, point& p) {
    j.at("x").get_to(p.x);
    j.at("y").get_to(p.y);
}
int main() {
    point p{3, 4};
    json j = p;                    // 自动调用 to_json
    std::cout << j.dump() << std::endl;   // {"x":3,"y":4}
    point p2 = j.get<point>();     // 自动调用 from_json
    std::cout << p2.x << "," << p2.y << std::endl;
}

这样写完后,std::vector<point> 转 json、json 转 std::vector<point> 也全都自动支持了,非常优雅。

4.6 异常处理(error handling)

nlohmann/json 的异常体系全部继承自 std::exception,所以你可以分级捕获:

try {
    auto j = json::parse(r"({"a": 1)");
} catch (const json::parse_error& e) {
    // 1. 解析失败:语法错误、非法 utf-8 等
    std::cout << "解析错误 byte " << e.byte << ": " << e.what() << std::endl;
} catch (const json::out_of_range& e) {
    // 2. at() 越界 / 键不存在
    std::cout << "越界: " << e.what() << std::endl;
} catch (const json::type_error& e) {
    // 3. 类型错误:get<t>() 类型不匹配、操作符用法错误
    std::cout << "类型错误: " << e.what() << std::endl;
} catch (const json::other_error& e) {
    // 4. 其他错误
    std::cout << "其他: " << e.what() << std::endl;
} catch (const std::exception& e) {
    // 5. 兜底
    std::cout << "通用异常: " << e.what() << std::endl;
}

⚠️ 预警:operator[] 在键不存在时不会抛异常(它会自动创建 null),所以"希望报错"的场景一定要用 .at()。很多线上 bug 都是因为 j["missing_key"] 静默返回 null 然后被当成 0 用。

4.7 性能优化技巧(performance tips)

nlohmann/json 的设计哲学是易用优先,性能在同级库中属于"够用但非顶尖"。如果 json 解析成为性能瓶颈,试试下面这些技巧:

// 技巧 1:减少深拷贝 —— 用引用而不是值
// ❌ 慢:每次取值都拷贝整个子对象
json copied = j["big_object"];           // 深拷贝!如果 big_object 很大,非常伤
// ✅ 快:用引用只读访问
const json& ref = j["big_object"];       // 零拷贝
// 技巧 2:批量取值用 get_to,避免多次类型转换开销
int x = 0, y = 0;
j["point"]["x"].get_to(x);
j["point"]["y"].get_to(y);               // get_to 直接写入变量,省一次临时对象
// 技巧 3:重复解析同一字符串时,用 sax 接口(流式回调,不建整棵树)
// 适合"从超大 json 里只挑几个字段"的场景
struct myhandler : json::parser_callback_t {
    bool operator()(int depth, json::parse_event_t event, json& parsed) override {
        // 每遇到一个 key/value 就会回调,可以在这里挑需要的字段
        return true;   // 返回 false 可以提前终止解析
    }
};
json::parser_callback_t cb = myhandler();
// json::parse(str, cb, true, true);   // 传入回调开启 sax 模式
// 技巧 4:对大 json 提前 reserve 容量(v3.11+ 支持)
json::parser_callback_t cb2 = nullptr;
// 解析前如果知道大概大小,可调用 j.reserve(n) 减少重新分配
// 技巧 5:终极优化 —— 换用 rapidjson(见下文对比表)
// 如果解析吞吐量是硬指标,nlohmann/json 不是最快的,但它通常是"足够快"的。

⚠️ 预警不要把 json::parse 放进热点循环里反复解析同一段文本。如果同一响应要解析 n 次,请解析一次、复用 json 对象。另外 dump() 默认会做严格转义(\uxxxx),如果只是要最小化输出可以用 dump(-1, ' ', false, json::error_handler_t::replace) 等参数微调。

五、对比表格:nlohmann/json vs rapidjson vs boost.propertytree

维度nlohmann/jsonrapidjsonboost.propertytree
核心定位现代 c++ 易用 json 库极致性能 json 库通用属性树(json 只是其中一种格式)
安装难度⭐ 极低(单头文件)中(需要配置,含可选内存池)低(boost 全家桶自带)
api 风格像 python dict 一样自然c 风格偏底层,要手写 document 生命周期树形 get/put,略笨重
类型安全⭐ 强(异常 + 类型判断)弱(全靠文档约定,错误易漏)中(get 模板,但行为粗糙)
stl 互转⭐ 直接互转 vector/map/optional需要自己写转换函数只支持少数基础类型
性能中(足够快)⭐ 极高(最快梯队)低(有较大开销)
c++ 标准要求c++11+c++11+(老版本 c++03 也有)c++11+
异常安全好(所有错误都抛异常)一般(大量场景需手动检查返回码)
依赖依赖 boost 核心
适合人群90% 的日常开发对性能极致的底层服务老项目 / 不想装新库
维护活跃度高(v3.11+ 仍持续更新)较高(但开发节奏放缓)随 boost 版本走

一句话选型建议:

  • 默认选 nlohmann/json:90% 的场景它都是最省心的;
  • 需要每秒钟解析上百万次、或内存敏感(嵌入式)→ 选 rapidjson
  • 项目里已经深度使用 boost、不想引入新依赖 → 选 boost.propertytree

5.1 json 解析库选型速查表

你的需求推荐理由
想快速上手、代码可读性优先nlohmann/jsonapi 最像现代语言
极致吞吐量 / 嵌入式rapidjson内存池 + 零拷贝 dom
已经用 boostboost.propertytree顺手,但功能有限
需要 json schema 校验nlohmann/json(官方支持 json schema)内置 json_schema_validator(实验性)
需要流式解析超大文件simdjson 或 rapidjson sax不做整棵 dom 树
只需序列化不解析nlohmann/json 或 {fmt} + 手写简单场景够用
全平台 + 单头文件nlohmann/json无平台差异坑

六、常见问题 faq 速查表

问题答案
q1:编译报错找不到头文件?确认 json.hpp 是否放在 include/nlohmann/ 下,且 include 路径已配置。msvc 记得加 /ehsc。
q2:j["key"] 取不存在的键为什么不报错?这是设计行为:operator[] 会自动创建 null 成员。只读场景请用 .at()(抛异常)或 .find()(不改变结构)。
q3:字符串 "123" 能直接 get<int>() 吗?不能,会抛 type_error。需先 get<std::string>() 再 std::stoi。
q4:json 里有注释能解析吗?默认不能。用 json::parse(str, nullptr, true, true) 开启 ignore_comments。
q5:对象和数组怎么区分?j.is_object() vs j.is_array()。初始化列表 {{"k",v}} 默认是对象;json::array() 可强制数组。
q6:dump() 输出中文会变成 \uxxxx 吗?默认会转义(ensure_ascii=true)。需要原样输出中文传 j.dump(-1, ' ', false, json::error_handler_t::replace)(第 4 个参数 ensure_ascii=false)。
q7:解析超大 json 文件内存爆了怎么办?换 sax 流式回调(parser_callback_t),或换 simdjson / rapidjson 的流式接口。
q8:性能不够,有什么无损升级路径?先用引用避免拷贝 + get_to 批量取;仍不够再考虑 rapidjson。注意两者 api 完全不同,需改代码。
q9:怎么让自定义类支持 json 转换?定义 to_json / from_json 两个全局函数(见 4.5 节),之后容器嵌套也能自动转。
q10:能解析不合法 utf-8 吗?默认严格模式会抛 parse_error;可用 error_handler_t::replace 替换非法字节继续解析。
q11:多线程环境安全吗?json 对象本身不是线程安全的(和 stl 容器一样)。不同线程操作不同对象没问题;共享同一对象需要加锁。
q12:编译时间太长怎么办?只在少数文件 include;把常用 json 操作封装到一个翻译单元;或考虑 pch(预编译头)。

七、总结

nlohmann/json 之所以成为 c++ 社区最受欢迎的 json 库,不是因为它最快,而是因为它把 c++ 处理 json 的痛苦降到了最低:单头文件零配置、stl 容器无缝互转、异常处理友好、代码像 python 一样好读。它适合 90% 的日常场景——配置文件、网络报文、对象持久化、测试数据构造,几乎无处不在。

最后送你三句口诀:

  1. :json::parse 读进来,at() 安全取;
  2. :初始化列表构建,dump() 序列化出去;
  3. 避坑:operator[] 会自动造键,只读请用 at() / find()。

到此这篇关于nlohmann/json 深度解析:让 c++ 解析 json 像喝水一样简单的文章就介绍到这了,更多相关 c++ 解析 json内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com