nlohmann json lib
nlohmann json Modern C++ JSON lib
摘要
本文档全面介绍了 nlohmann/json
,一个广受欢迎的现代C++ JSON库。我们将从其起源与发展入手,探讨其在C++生态系统中的地位,并与其他主流C++ JSON库进行比较。随后,深入剖析 nlohmann/json
的架构与设计哲学,揭示其简洁易用的特点。最后,文档将详细阐述 nlohmann/json
在C++项目中的具体应用,包括安装、基本语法、核心API以及高级用法,旨在为C++开发者提供一份权威、实用的参考指南。
关键词: C++, JSON, nlohmann/json, 数据序列化, 反序列化, 开源库
1. nlohmann/json 简介
1.1 nlohmann的起源与发展
nlohmann/json
库由Niels Lohmann于2013年启动开发,其设计初衷是为C++提供一个现代、直观且功能完备的JSON处理解决方案。在JSON成为Web应用和数据交换事实标准的背景下,C++社区对一个易于使用且性能良好的JSON库的需求日益增长。早期的C++ JSON库往往存在API复杂、学习曲线陡峭或依赖外部库过多的问题。nlohmann/json
的出现,以其仅头文件、无需编译、API设计与标准库容器(如 std::vector
和 std::map
)高度契合的特点,迅速获得了C++开发者的青睐。
自发布以来,nlohmann/json
持续迭代,不断完善功能、提升性能并修复潜在问题。它在GitHub上拥有庞大的社区支持和活跃的开发,已成为C++生态系统中处理JSON数据的首选库之一,广泛应用于各种项目,包括嵌入式系统、高性能服务器、桌面应用等。
1.2 与其他C++ JSON库的比较
C++社区拥有多种JSON处理库,各有优劣。下表对 nlohmann/json
与其他几个主流C++ JSON库进行了比较:
特性/库 | nlohmann/json | RapidJSON | Boost.PropertyTree | jsoncpp |
API设计 | 直观,类似STL容器操作 | DOM/SAX,性能导向 | 树形结构,通用数据 | DOM,传统C++风格 |
易用性 | 极高,学习曲线平缓 | 中等,需要理解DOM/SAX模型 | 中等,需要理解属性树概念 | 中等,较为传统 |
性能 | 良好,满足大多数应用 | 极高,注重解析和序列化速度 | 中等,通用性带来的开销 | 中等 |
依赖 | 仅头文件,无外部依赖 | 仅头文件,无外部依赖 | Boost库,依赖较重 | 需编译,无外部依赖 |
功能完备性 | 完备,支持各种JSON操作 | 完备,支持各种JSON操作 | 完备,但JSON仅是其一种格式 | 完备 |
内存效率 | 良好,但可能不如RapidJSON极致 | 极高,池分配器优化 | 中等 | 中等 |
编译时间 | 快速(仅头文件) | 快速(仅头文件) | 较慢(Boost库) | 较慢(需编译) |
总结:
nlohmann/json
: 适用于追求开发效率、代码简洁性和易用性的项目。其API设计哲学使得C++开发者能够以自然的方式操作JSON数据。RapidJSON
: 适用于对性能有极致要求的场景,如大数据处理、高并发服务器等。但其API相对底层,学习成本较高。Boost.PropertyTree
: 提供更通用的树状数据结构处理能力,JSON只是其支持的一种序列化格式。适用于需要处理多种配置或数据格式的复杂系统。jsoncpp
: 一个成熟但相对传统的C++ JSON库,其API风格可能不如nlohmann/json
现代和直观。
1.3 nlohmann的架构与设计理念
nlohmann/json
的核心设计理念是**“像使用标准库容器一样使用JSON”**。它通过重载运算符和提供类似STL的接口,使得JSON对象的创建、访问和修改变得异常直观。
其架构主要基于一个 basic_json
模板类,该类可以根据需要进行特化,以支持不同类型和自定义的分配器。默认的 json
类型是 basic_json<std::map, std::vector, std::string, bool, int64_t, uint64_t, double, std::allocator>
的别名,它代表了JSON数据模型中的六种基本类型:null、布尔值、数字(整数和浮点数)、字符串、数组和对象。
设计原则:
- 仅头文件 (Header-Only):
nlohmann/json
仅由一个头文件 (json.hpp
) 组成,无需编译即可直接包含到项目中,极大地简化了集成过程。 - 强类型安全 (Strong Type Safety): 尽管JSON本身是无模式的,但
nlohmann/json
在运行时提供了类型检查机制。尝试以错误类型访问JSON值会抛出异常,有助于在开发阶段发现问题。 - 直观的API: 借鉴C++标准库的设计思想,通过重载
operator[]
、operator=
、push_back
等,使得JSON操作与C++容器操作保持一致性。 - 异常安全 (Exception Safety): 库在发生错误时(如类型不匹配、键不存在等)会抛出
json::exception
或其派生类,允许开发者使用try-catch
机制进行错误处理。 - 透明的序列化/反序列化 (Transparent Serialization/Deserialization): 通过集成
operator<<
和operator>>
,使得JSON数据的I/O操作与C++流操作无缝衔接。 - 支持自定义类型 (Support for Custom Types): 提供了
to_json
和from_json
机制,允许开发者轻松地将自定义C++对象序列化为JSON,以及将JSON反序列化为自定义对象。 - C++11及更高版本兼容: 充分利用C++11及后续标准的新特性,如右值引用、
auto
关键字、列表初始化等,使得代码更加现代和高效。
2. nlohmann在C++中的应用
2.1 nlohmann的安装与引用
由于 nlohmann/json
是一个仅头文件的库,其安装和引用过程非常简单。
2.1.1 安装
手动下载
直接从GitHub仓库下载最新的 json.hpp 文件:https://github.com/nlohmann/json/releases
将其放置在您的C++项目可以找到的某个包含路径下。
CMake
如果您的项目使用CMake,可以通过 FetchContent 或 add_subdirectory 来集成。
使用 FetchContent (推荐):
include(FetchContent)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2 # 或其他最新版本
)
FetchContent_MakeAvailable(nlohmann_json)
# 在您的目标中链接
target_link_libraries(YourProject PRIVATE nlohmann_json::nlohmann_json)
包管理器
- Conan: 在
conanfile.txt
或conanfile.py
中添加nlohmann_json/x.y.z
。 - vcpkg:
vcpkg install nlohmann-json
- Homebrew (macOS/Linux):
brew install nlohmann-json
2.1.2 引用
在C++源文件中,只需简单地包含 json.hpp
头文件即可:
#include <iostream>
#include "json.hpp" // 如果json.hpp在项目根目录或指定包含路径下
// 或者 #include <nlohmann/json.hpp> 如果通过CMake或包管理器安装
为了方便使用 nlohmann::json
类型,通常会使用 using
声明:
using json = nlohmann::json;
2.2 基本语法
nlohmann/json
提供了与C++标准库容器相似的直观API,使得JSON数据的操作变得非常自然。
2.2.1 JSON对象的创建与初始化
#include <iostream>
#include "json.hpp"
using json = nlohmann::json;
int main() {
// 1. 空JSON对象
json j_empty_object = json::object();
json j_empty_object_alt = {}; // 使用列表初始化创建空对象
// 2. 空JSON数组
json j_empty_array = json::array();
json j_empty_array_alt = json::parse("[]"); // 从字符串解析空数组
// 3. 直接初始化JSON对象
json j_object = {
{"name", "Alice"},
{"age", 30},
{"isStudent", false},
{"courses", {"Math", "Physics", "Chemistry"}},
{"address", {
{"street", "123 Main St"},
{"city", "Anytown"}
}}
};
std::cout << "Initialized JSON Object:\n" << j_object.dump(4) << std::endl;
// 4. 直接初始化JSON数组
json j_array = {"apple", 123, true, 3.14, nullptr};
std::cout << "\nInitialized JSON Array:\n" << j_array.dump(4) << std::endl;
return 0;
}
2.2.2 访问JSON元素
- 通过键名访问 (对象): 使用
operator[]
或at()
。at()
在键不存在时会抛出异常,而operator[]
会创建新键。 - 通过索引访问 (数组): 使用
operator[]
或at()
。 - 类型检查:
is_string()
,is_number()
,is_boolean()
,is_array()
,is_object()
,is_null()
。 - 类型转换: 使用
get<T>()
或隐式转换。
#include <iostream>
#include "json.hpp"
using json = nlohmann::json;
int main() {
json data = {
{"name", "Bob"},
{"age", 25},
{"city", "New York"},
{"interests", {"coding", "reading", "hiking"}},
{"details", {
{"is_active", true},
{"member_since", 2020}
}},
{"null_value", nullptr}
};
// 访问对象元素
std::cout << "Name: " << data["name"] << std::endl; // Bob
std::cout << "Age: " << data.at("age") << std::endl; // 25
// 访问数组元素
std::cout << "First interest: " << data["interests"][0] << std::endl; // coding
std::cout << "Second interest (using at()): " << data.at("interests").at(1) << std::endl; // reading
// 访问嵌套元素
std::cout << "Is active: " << data["details"]["is_active"] << std::endl; // true
// 类型检查
if (data["age"].is_number()) {
std::cout << "Age is a number." << std::endl;
}
if (data["interests"].is_array()) {
std::cout << "Interests is an array." << std::endl;
}
if (data["null_value"].is_null()) {
std::cout << "Null value is null." << std::endl;
}
// 类型转换
std::string name = data["name"].get<std::string>();
int age = data["age"]; // 隐式转换
bool isActive = data["details"]["is_active"];
std::cout << "Name (converted): " << name << std::endl;
std::cout << "Age (converted): " << age << std::endl;
std::cout << "Is active (converted): " << isActive << std::endl;
// 访问不存在的键 (operator[] 会创建,at() 会抛异常)
std::cout << "Trying to access non-existent key (operator[]): " << data["non_existent_key"] << std::endl;
// std::cout << data.at("another_non_existent_key") << std::endl; // 这会抛出 json::out_of_range 异常
return 0;
}
2.2.3 修改JSON元素
#include <iostream>
#include "json.hpp"
using json = nlohmann::json;
int main() {
json profile = {
{"name", "Charlie"},
{"age", 28},
{"email", "charlie@example.com"}
};
std::cout << "Original profile:\n" << profile.dump(4) << std::endl;
// 修改现有值
profile["age"] = 29;
profile["email"] = "charlie.new@example.com";
// 添加新键值对
profile["occupation"] = "Software Engineer";
// 添加新数组元素
profile["skills"] = {"C++", "Python", "JavaScript"};
profile["skills"].push_back("Go");
// 修改嵌套对象
profile["address"] = {
{"street", "456 Oak Ave"},
{"zip", "10001"}
};
profile["address"]["city"] = "San Francisco";
std::cout << "\nModified profile:\n" << profile.dump(4) << std::endl;
return 0;
}
2.2.4 遍历JSON
nlohmann/json
支持基于范围的 for
循环遍历JSON对象和数组。
#include <iostream>
#include "json.hpp"
using json = nlohmann::json;
int main() {
json data = {
{"id", 101},
{"title", "Sample Document"},
{"tags", {"programming", "json", "c++"}},
{"contributors", {
{"name", "Alice"}, {"id", 1}
}},
{"contributors", {
{"name", "Bob"}, {"id", 2}
}}
};
std::cout << "Iterating over JSON object (key-value pairs):\n";
for (json::iterator it = data.begin(); it != data.end(); ++it) {
std::cout << " Key: " << it.key() << ", Value: " << it.value() << std::endl;
}
// 或者使用基于范围的 for 循环 (更简洁)
// for (auto const& [key, val] : data.items()) { // C++17 结构化绑定
// std::cout << " Key: " << key << ", Value: " << val << std::endl;
// }
std::cout << "\nIterating over JSON array (tags):\n";
for (const auto& tag : data["tags"]) {
std::cout << " Tag: " << tag << std::endl;
}
std::cout << "\nIterating over array of objects (contributors):\n";
// 注意:如果"contributors"是对象,则第二次赋值会覆盖第一次。
// 这里假设它是一个数组,演示迭代数组中的对象
// 重新构建data,确保contributors是数组
json actual_data = {
{"id", 101},
{"title", "Sample Document"},
{"tags", {"programming", "json", "c++"}},
{"contributors", json::array({
{{"name", "Alice"}, {"id", 1}},
{{"name", "Bob"}, {"id", 2}}
})}
};
for (const auto& contributor : actual_data["contributors"]) {
std::cout << " Contributor Name: " << contributor["name"]
<< ", ID: " << contributor["id"] << std::endl;
}
return 0;
}
2.3 核心 API
nlohmann/json
提供了丰富的核心API,用于JSON数据的解析、序列化、查询、操作和类型转换。
2.3.1 解析与序列化
- 解析 (Parsing): 从字符串、文件流等解析JSON数据。
json::parse(source)
: 从字符串或输入流解析。json::accept(source)
: 检查字符串是否是有效的JSON而无需完整解析。
- 序列化 (Serialization): 将JSON数据转换为字符串。
dump(indent = -1, indent_char = ' ', ensure_ascii = false)
: 将JSON序列化为字符串。indent
参数用于美化输出,-1
表示不美化。
#include <iostream>
#include <fstream>
#include "json.hpp"
using json = nlohmann::json;
int main() {
// 从字符串解析
std::string json_str = R"({"product": "Laptop", "price": 1200.50, "inStock": true})";
json product_data = json::parse(json_str);
std::cout << "Parsed from string:\n" << product_data.dump(4) << std::endl;
// 从文件解析
// 假设存在一个名为 "config.json" 的文件
// {"setting1": "value1", "setting2": 123}
std::ofstream ofs("config.json");
ofs << R"({"setting1": "value1", "setting2": 123})";
ofs.close();
std::ifstream ifs("config.json");
if (ifs.is_open()) {
json config;
ifs >> config; // 使用运算符重载进行解析
std::cout << "\nParsed from file:\n" << config.dump(4) << std::endl;
ifs.close();
} else {
std::cerr << "Error: Could not open config.json" << std::endl;
}
// 序列化为字符串 (美化输出)
std::string pretty_json = product_data.dump(2); // 2个空格缩进
std::cout << "\nPretty JSON:\n" << pretty_json << std::endl;
// 序列化为紧凑字符串 (无缩进)
std::string compact_json = product_data.dump(); // 默认无缩进 (indent = -1)
std::cout << "\nCompact JSON:\n" << compact_json << std::endl;
return 0;
}
2.3.2 查找与查询
count(key)
: 检查JSON对象中是否存在某个键。contains(key)
: 检查JSON对象中是否存在某个键 (C++20)。find(key)
: 返回指向键值对的迭代器,如果不存在则返回end()
。value(key, default_value)
: 安全地获取某个键的值,如果键不存在则返回默认值。
#include <iostream>
#include "json.hpp"
using json = nlohmann::json;
int main() {
json user = {
{"id", 1001},
{"username", "johndoe"},
{"email", "john@example.com"},
{"roles", {"admin", "editor"}}
};
// 检查键是否存在
if (user.count("username")) {
std::cout << "Username exists: " << user["username"] << std::endl;
}
if (user.contains("email")) { // C++20 及以上版本可用
std::cout << "Email exists: " << user["email"] << std::endl;
}
// 使用 find()
auto it = user.find("roles");
if (it != user.end()) {
std::cout << "Roles found: " << *it << std::endl;
} else {
std::cout << "Roles not found." << std::endl;
}
auto non_existent_it = user.find("password");
if (non_existent_it == user.end()) {
std::cout << "Password not found (as expected)." << std::endl;
}
// 使用 value() 安全获取
std::string city = user.value("city", "Unknown"); // city不存在,返回"Unknown"
std::cout << "User's city: " << city << std::endl;
std::string username = user.value("username", "Guest"); // username存在,返回实际值
std::cout << "User's username: " << username << std::endl;
return 0;
}
2.3.3 修改与删除
clear()
: 清空JSON对象或数组。erase()
: 删除元素。push_back()
: 向数组添加元素。emplace()
: 在对象中插入元素。update()
: 合并或更新JSON对象。
#include <iostream>
#include "json.hpp"
using json = nlohmann::json;
int main() {
json settings = {
{"theme", "dark"},
{"fontSize", 14},
{"notifications", true},
{"features", {"darkMode", "autoSave"}}
};
std::cout << "Original settings:\n" << settings.dump(4) << std::endl;
// 修改值
settings["fontSize"] = 16;
// 添加新键值对
settings["language"] = "en-US";
// 删除键值对
settings.erase("notifications"); // 按键删除
// 或者 settings.erase(settings.find("notifications")); // 按迭代器删除
// 向数组添加元素
settings["features"].push_back("spellCheck");
settings["features"].insert(settings["features"].begin(), "hotReload"); // 插入到开头
// 清空数组
// settings["features"].clear();
std::cout << "\nModified settings:\n" << settings.dump(4) << std::endl;
// update() 合并对象
json new_defaults = {
{"language", "zh-CN"}, // 会覆盖
{"defaultFolder", "/home/user/docs"}, // 会添加
{"fontSize", 12} // 会覆盖
};
settings.update(new_defaults);
std::cout << "\nSettings after update:\n" << settings.dump(4) << std::endl;
return 0;
}
2.4 高级语法
2.4.1 自定义类型序列化与反序列化
nlohmann/json
提供了强大的机制,允许开发者轻松地将自定义C++结构体或类与JSON数据进行双向转换,无需手动编写复杂的解析和构建逻辑。这通过为自定义类型重载 to_json
和 from_json
函数来实现。
#include <iostream>
#include "json.hpp"
using json = nlohmann::json;
// 定义一个自定义结构体
struct Person {
std::string name;
int age;
std::vector<std::string> hobbies;
bool is_student;
};
// 为 Person 类型提供 to_json 函数(将 Person 转换为 json)
void to_json(json& j, const Person& p) {
j = json{{"name", p.name},
{"age", p.age},
{"hobbies", p.hobbies},
{"is_student", p.is_student}};
}
// 为 Person 类型提供 from_json 函数(将 json 转换为 Person)
void from_json(const json& j, Person& p) {
j.at("name").get_to(p.name);
j.at("age").get_to(p.age);
j.at("hobbies").get_to(p.hobbies);
j.at("is_student").get_to(p.is_student);
}
int main() {
// 1. 将 C++ 对象序列化为 JSON
Person alice = {"Alice", 25, {"reading", "cycling"}, true};
json j_alice = alice; // 自动调用 to_json(j_alice, alice)
std::cout << "Alice as JSON:\n" << j_alice.dump(4) << std::endl;
// 2. 将 JSON 反序列化为 C++ 对象
json j_bob = R"(
{
"name": "Bob",
"age": 30,
"hobbies": ["gaming", "hiking"],
"is_student": false
}
)"_json; // 使用 _json 字面量操作符解析字符串
Person bob = j_bob.get<Person>(); // 自动调用 from_json(j_bob, bob)
std::cout << "\nBob (from JSON):\n";
std::cout << "Name: " << bob.name << std::endl;
std::cout << "Age: " << bob.age << std::endl;
std::cout << "Hobbies: ";
for (const auto& hobby : bob.hobbies) {
std::cout << hobby << " ";
}
std::cout << "\nIs Student: " << (bob.is_student ? "Yes" : "No") << std::endl;
// 错误处理:如果 JSON 缺少某个字段,from_json 会抛出异常
json invalid_json = R"({"name": "Eve", "age": 22})"_json;
try {
Person eve = invalid_json.get<Person>();
} catch (const json::exception& e) {
std::cerr << "\nError deserializing invalid_json: " << e.what() << std::endl;
}
return 0;
}
2.4.2 JSON Pointer
JSON Pointer (RFC 6901) 提供了一种标准化的方式来在JSON文档中定位特定的值。nlohmann/json
完全支持JSON Pointer。
#include <iostream>
#include "json.hpp"
using json = nlohmann::json;
int main() {
json doc = R"(
{
"foo": ["bar", "baz"],
"": 0,
"a/b": 1,
"c%d": 2,
"e^f": 3,
"g|h": 4,
"i\\j": 5,
"k\"l": 6,
" ": 7,
"m~n": 8,
"object": {
"nested_key": "nested_value"
},
"array": [
{"item1": "value1"},
{"item2": "value2"}
]
}
)"_json;
// 使用 JSON Pointer 访问元素
std::cout << "doc[\"/foo/0\"]: " << doc.at("/foo/0") << std::endl; // "bar"
std::cout << "doc[\"/a~1b\"]: " << doc.at("/a~1b") << std::endl; // 1 (斜杠编码为 ~1)
std::cout << "doc[\"/m~0n\"]: " << doc.at("/m~0n") << std::endl; // 8 (波浪号编码为 ~0)
std::cout << "doc[\"/object/nested_key\"]: " << doc.at("/object/nested_key") << std::endl; // "nested_value"
std::cout << "doc[\"/array/0/item1\"]: " << doc.at("/array/0/item1") << std::endl; // "value1"
// JSON Pointer 赋值
doc["/foo/1"] = "qux";
std::cout << "\nModified doc (foo/1):\n" << doc.dump(4) << std::endl;
// JSON Pointer 添加新值
doc["/new_key"] = "new_value";
std::cout << "\nModified doc (new_key):\n" << doc.dump(4) << std::endl;
// 检查是否存在
if (doc.contains("/object/nested_key")) {
std::cout << "\n'/object/nested_key' exists." << std::endl;
}
// 错误处理:访问不存在的 JSON Pointer
try {
std::cout << doc.at("/non_existent_path") << std::endl;
} catch (const json::exception& e) {
std::cerr << "Error accessing non-existent path: " << e.what() << std::endl;
}
return 0;
}
2.4.3 JSON Patch
JSON Patch ([[RFC 6902]]) 定义了一种标准的数据格式,用于描述对JSON文档的更改。nlohmann/json
提供了对JSON Patch 的支持,包括生成和应用补丁。
#include <iostream>
#include "json.hpp"
using json = nlohmann::json;
int main() {
json document = R"(
{
"baz": "qux",
"foo": ["bar", "baz"],
"glossary": {
"title": "example glossary"
}
}
)"_json;
json other_document = R"(
{
"baz": "boo",
"foo": ["bar", "boo"],
"test": "hello",
"glossary": {
"title": "updated glossary"
}
}
)"_json;
std::cout << "Original Document:\n" << document.dump(4) << std::endl;
std::cout << "\nOther Document:\n" << other_document.dump(4) << std::endl;
// 1. 生成 JSON Patch
// 计算从 document 到 other_document 的差异
json patch = json::diff(document, other_document);
std::cout << "\nGenerated JSON Patch:\n" << patch.dump(4) << std::endl;
// 2. 应用 JSON Patch
json patched_document = document.patch(patch);
std::cout << "\nDocument after applying patch:\n" << patched_document.dump(4) << std::endl;
// 验证补丁是否正确应用
if (patched_document == other_document) {
std::cout << "\nPatched document matches other_document." << std::endl;
} else {
std::cout << "\nPatched document does NOT match other_document." << std::endl;
}
// 手动创建 JSON Patch
json manual_patch = json::array({
{{"op", "replace"}, {"path", "/baz"}, {"value", "new_qux"}},
{{"op", "add"}, {"path", "/new_key"}, {"value", 123}},
{{"op", "remove"}, {"path", "/foo/0"}}
});
json doc_to_patch = document; // 复制一份进行操作
try {
doc_to_patch = doc_to_patch.patch(manual_patch);
std::cout << "\nDocument after applying manual patch:\n" << doc_to_patch.dump(4) << std::endl;
} catch (const json::exception& e) {
std::cerr << "Error applying manual patch: " << e.what() << std::endl;
}
return 0;
}
结论
nlohmann/json
库凭借其仅头文件、直观的API设计、强大的功能集以及对C++现代特性的充分利用,已成为C++处理JSON数据的首选解决方案。它不仅简化了JSON的解析和序列化过程,还通过支持JSON Pointer 和 JSON Patch 等高级特性,极大地提升了C++在复杂数据操作方面的能力。无论是小型配置解析还是大型系统间的数据交换,nlohmann/json
都能提供一个高效、可靠且易于维护的工具。随着C++标准的不断演进,nlohmann/json
也将继续保持其领先地位,为C++开发者提供更加便捷和强大的JSON处理体验。
参考文献
- [1] nlohmann/json GitHub Repository:
https://github.com/nlohmann/json
- [2] JSON (JavaScript Object Notation) - ECMA-404 The JSON Data Interchange Format:
https://www.ecma-international.org/publications-and-standards/standards/ecma-404/
- [3] RFC 6901 - JSON Pointer:
https://tools.ietf.org/html/rfc6901
- [4] RFC 6902 - JSON Patch:
https://tools.ietf.org/html/rfc6902