在 C++ 中,当程序处理完业务逻辑后,我们经常需要把内存里的字符串、数字、布尔值或容器数据组装成标准的 JSON 文本,用来保存配置文件或返回给前端接口。这种将程序中的数据转换成 JSON 文本的过程,通常叫做 “序列化”。
初学的小伙伴千万不要使用 std::string 手动去拼接花括号、双引号和逗号。一旦数据出现多层嵌套或特殊字符,手动拼接不仅繁琐,还特别容易漏写引号或逗号。与前面解析 JSON 时一样,组装和生成 JSON 数据同样借助单头文件库 nlohmann/json 来完成。
创建 JSON 对象并输出:dump()
把前面准备好的 json.hpp 放在本地项目目录后,我们在代码开头引入该头文件,并加上 using json = nlohmann::json; 简化类型书写。创建一个 JSON 对象最直观的方式,是先调用 json::object() 创建一个空对象,再通过中括号逐个写入字段,最后调用 dump() 导出为字符串。
语法:
json data = json::object();
data["键名"] = 值;
std::string compactText = data.dump();
std::string prettyText = data.dump(缩进空格数);说明:
前面读取外部 JSON 数据时,我们使用 at() 是为了在字段缺失时及时报错。而现在是由我们的程序主动创建新数据,直接使用中括号 data["键名"] = 值; 会方便得多:当键名不存在时,它会自动新建该字段并赋值;当键名已经存在时,则会直接覆盖原来的值。
dump 在英文中是 “倾倒、导出” 的意思。不带参数的 data.dump() 会生成没有多余空格和换行的紧凑单行文本,体积最小,适合网络传输。如果传入整数参数(比如写成 data.dump(4)),则会使用 4 个空格缩进排版,生成方便阅读的多行文本。
示例 1:创建并输出 JSON 对象
#include <iostream>
#include <string>
#include "json.hpp"
using json = nlohmann::json;
int main() {
json data = json::object();
data["name"] = "Jack";
data["age"] = 20;
data["active"] = true;
// 生成紧凑格式
std::string compactText = data.dump();
std::cout << "紧凑格式:\n" << compactText << "\n\n";
// 生成美化格式
std::string prettyText = data.dump(4);
std::cout << "美化格式:\n" << prettyText << '\n';
return 0;
}运行结果如下。
紧凑格式:
{"active":true,"age":20,"name":"Jack"}
美化格式:
{
"active": true,
"age": 20,
"name": "Jack"
}分析:
json data = json::object(); 先创建了一个空的 JSON 对象 data,接着通过中括号依次写入了 "name"、"age" 和 "active" 三个字段。调用 data.dump() 时,所有内容紧贴在同一行;调用 data.dump(4) 时,库会自动加上换行和 4 个空格缩进,层次结构一目了然。
除了先创建空对象再逐个赋值之外,当对象包含的字段已经固定时,我们也可以直接使用花括号初始化列表一步创建好 JSON 对象。对于上面这个例子来说,下面 2 种方式是等价的。
// 方式 1
json data = json::object();
data["name"] = "Jack";
data["age"] = 20;
data["active"] = true;
// 方式 2
json data = {
{"name", "Jack"},
{"age", 20},
{"active", true}
};在写入字段时,库会根据右侧 C++ 数据的类型自动生成对应的 JSON 类型(如需写入 JSON 空值 null,直接赋 C++ 的空指针 nullptr 即可)。常见 C++ 赋值写法与生成的 JSON 结果对应如下表所示。
| C++ 赋值写法 | 生成的 JSON 片段 | 结果类型说明 |
|---|---|---|
| data["age"] = 20; | "age": 20 | JSON 数字(可参与数值运算) |
| data["age"] = "20"; | "age": "20" | JSON 字符串(带双引号文本) |
| data["active"] = true; | "active": true | JSON 布尔值 |
| data["active"] = "true"; | "active": "true" | JSON 字符串(非布尔值) |
| data["remark"] = nullptr; | "remark": null | JSON 空值 |
提示 在示例 1 中,我们依次写入的是 name、age、active,但输出顺序却是 active、age、name。这是因为 nlohmann::json 内部默认按 “键名的字母顺序” 保存字段,而标准 JSON 对象本身是不依赖字段先后顺序的。
创建 JSON 数组与嵌套结构
在实际开发中,一份完整的 JSON 数据不仅有普通字段,还会包含数组和嵌套的子对象。如果我们想要创建一个 JSON 数组,既可以在调用 json::array() 时直接传入初始元素,也可以先创建空数组再调用 push_back() 逐个追加,或者在引入 <vector> 头文件后直接将 std::vector 容器赋值给 json 对象:
// 方式 1
json skills = json::array({"C++", "Python"});
// 方式 2
json scores = json::array();
scores.push_back(95);
scores.push_back(88);
// 方式 3
std::vector<std::string> vec = {"C++", "Python"};
json langs = vec;这里小伙伴们要注意区分空对象、空数组与默认空值:如果只写 json data; 而不赋任何初值,dump() 导出的是 null;只有调用 json::object() 才会生成空对象 {},调用 json::array() 才会生成空数组 []。
由于 JSON 的对象和数组支持自由嵌套,我们可以直接把准备好的子对象或数组赋值给外层对象的某个字段。下面我们来看一个例子。
示例 2:组装嵌套对象与数组
#include <iostream>
#include <string>
#include "json.hpp"
using json = nlohmann::json;
int main() {
// 准备嵌套的子对象与数组
json address = {
{"city", "广州"},
{"zip", 510000}
};
json skills = json::array({"C++", "Python"});
// 组装到外层 student 对象
json student = json::object();
student["name"] = "Jack";
student["address"] = address;
student["skills"] = skills;
std::cout << student.dump(4) << '\n';
return 0;
}运行结果如下。
{
"address": {
"city": "广州",
"zip": 510000
},
"name": "Jack",
"skills": [
"C++",
"Python"
]
}分析:
student["address"] = address; 和 student["skills"] = skills; 表示将准备好的子对象与数组直接挂载到外层的 student 对象下。对比上一节的解析过程可以看到,解析是顺着层级 “往里读”,而生成则是顺着层级 “往里装”,两者的结构完全对称。
将 JSON 保存到文件
当把内存中的 JSON 对象通过 dump(4) 序列化成字符串之后,我们就可以借助输出文件流 std::ofstream 将其写入 .json 文件中保存。请看下面例子。
示例 3:将 JSON 写入文件
#include <fstream>
#include <iostream>
#include <string>
#include "json.hpp"
using json = nlohmann::json;
int main() {
json student = json::object();
student["name"] = "Jack";
student["age"] = 20;
student["active"] = true;
student["address"] = {
{"city", "广州"},
{"zip", 510000}
};
student["skills"] = json::array({"C++", "Python"});
// 序列化为美化字符串
std::string text = student.dump(4);
// 打开文件并写入文本
std::ofstream file("student.json");
if (!file.is_open()) {
std::cout << "无法创建文件 student.json\n";
return 1;
}
file << text << '\n';
std::cout << "student.json 写入成功!\n";
return 0;
}运行结果如下。
student.json 写入成功!分析:
student.dump(4) 先把组装好的 student 对象转换为带缩进的字符串 text,然后我们通过 file << text << '\n'; 将其写入当前目录下的 student.json 文件。运行结束后打开该文件,可以看到里面的内容如下:
{
"active": true,
"address": {
"city": "广州",
"zip": 510000
},
"age": 20,
"name": "Jack",
"skills": [
"C++",
"Python"
]
}将自定义结构体转换为 JSON
对于 std::string、int、double、bool 以及 std::vector 等标准类型,nlohmann/json 已经内置了转换规则,可以直接赋值。但如果遇到自定义的结构体或类,C++ 编译器无法自动识别里面有哪些成员,也不知道每个成员应该对应哪个 JSON 键名。
遇到自定义结构体时,最直观、最常用的做法就是使用初始化列表,将结构体的各个成员显式映射到 json 对象的字段上。
示例 4:结构体转换为 JSON
#include <iostream>
#include <string>
#include "json.hpp"
using json = nlohmann::json;
struct Student {
std::string name;
int age;
bool active;
};
int main() {
Student s = {"Lucy", 21, true};
// 将结构体成员映射到 JSON 字段
json data = {
{"name", s.name},
{"age", s.age},
{"active", s.active}
};
std::cout << data.dump(4) << '\n';
return 0;
}运行结果如下。
{
"active": true,
"age": 21,
"name": "Lucy"
}分析:
在这个例子中,我们先定义了一个 Student 结构体变量 s,随后通过初始化列表把 s.name、s.age 和 s.active 一一对应地赋给了 JSON 的字段。
这里小伙伴们要注意,不能直接把整个自定义结构体变量赋值给 json 对象。下面这种写法就是错误的,编译时会直接报错。
// 错误方式
Student s = {"Lucy", 21, true};
json badData = s;简单来说,生成 JSON 并不是把 C++ 结构体在内存里的数据直接复制出来,而是把结构体里的各个成员按明确键值组装成 JSON 对象。
