基于nlohmann/json 实现 从C++对象转换成JSON数据格式
C++对象的JSON序列化与反序列化
基于JsonCpp库实现C++对象序列化与反序列化
JSON 介绍
JSON作为一种轻量级的数据交换格式,在Web服务和应用程序中广泛使用。
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。它基于JavaScript的一个子集,但是JSON是独立于语言的,许多编程语言都支持JSON格式数据的生成和解析。
- JSON数据结构
两个关键点: 对象和数组
对象由键值对组成 键 -> 字符串 值->字符串(string),数字(number),布尔值(boolean),对象(object)(就可以组成对象的对象,一直嵌套下去)或null
数组由有序的值的集合组成
eg.
{ "_comment": "2025年配置文件版本v2.0 - 数据源:企业ERP系统", "company": "Tech Corp", "departments": [ { "_comment": "部门对象注释 - name:部门名称(必填),employees:员工列表(至少1人)", "name": "Engineering", "employees": [ { "id": 101, "name": "Alice", "skills": ["Java", "Python", "Docker"], "isManager": false, "contact": { "email": "alice@tech.com", "phone": ["123-456-7890", "987-654-3210"] } } ] } ] }
2.JSON 与 XML 比较
在SQL Server 2014这本书上,我看见看XML的身影:
维度 | JSON优势场景 | XML优势场景 |
速度 | 高频数据交换(如实时API) | 非性能敏感场景(如文档存储) |
复杂度 | 简单到中等数据结构 | 多层次嵌套、需验证的复杂数据 |
生态 | 现代Web开发、JavaScript生态 | 传统企业系统、跨平台协议(如RSS) |
JSON与XML(Extensible Markup Language)都是用于数据交换的有效格式。与XML相比,JSON更加轻量级,结构简单,解析速度快,且易于在JavaScript环境中操作。由于这些优势,JSON在Web API设计中越来越受欢迎,逐渐取代了XML成为主流的数据交换格式。
JSON 字符串与C++对象转换
在c++后端开发中,我们会有需求: 从c++对象与JSON字符串的相互转换
JSON作为一种轻量级的数据交换格式,与C++等强类型语言之间的转换,涉及了数据模型的适配与映射问题。
JSON数据模型与C++数据模型对比
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,JSON数据模型的核心是键值对集合,即对象。
C++是一种静态类型、编译式语言,具备复杂的类型系统,包含结构体(Structs)、类(Classes)以及各种基本数据类型和容器。
显而易见,两种数据模型之间进行转换,我们需要将JSON对象转换为C++的类或结构体实例,反之亦然。由于JSON是动态的,通常需要为每个JSON对象创建一个相应的C++类,这个类定义了与JSON对象相对应的成员变量,并提供序列化与反序列化的接口。
JSON 对象序列化与反序列化原理
对象的序列化是指将对象的状态信息转换为可以存储或传输的形式的过程
数据转换机制
JSON与C++类型系统的映射是序列化的基础,主要依赖以下机制实现:
基本类型映射
JSON的number对应C++的int/double,string对应std::string,boolean对应bool,null通过std::optional或指针类型处理。
容器类型映射
JSON数组可映射到std::vector、std::list等线性容器,对象映射到std::map或自定义结构体。例如,{"skills": ["C++", "Python"]}可自动转换为std::vector。
自定义对象映射
侵入式:通过类的to_json/from_json方法手动定义字段映射关系。
非侵入式:利用宏(如NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE)自动生成映射代码,通过反射获取成员变量信息。
静态反射:借助Boost.PFR或结构化绑定,在编译期遍历结构体成员,实现零代码入侵的序列化。
关键步骤
遍历对象的成员变量,将它们转换为JSON可识别的键值对格式。
对于基本数据类型,直接转换即可;对于复杂类型,则需要递归调用序列化函数。反序列化的过程则相反,解析JSON字符串,并根据解析结果创建C++对象的实例。
接下来我们来看代码
转换实践案例(基于nlohmann/json)
首先,我的MinGW中没有默认有json库,本人在GitHub上找到了该库,将其载入我的cpp配置文件,更新我的配置文档
库函数位置:nlohmann/json
而后更新我的
我们来看代码:
#include using json = nlohmann::json; using namespace std; // 结构体定义 struct Person { string name; int age; bool isStu; }; // 序列化适配器的实现 void to_json(json& j, const Person& p) { j = json{{"name", p.name}, {"age", p.age}, {"isStu", p.isStu}}; } // 反序列化适配器的实现 void fron_json(const json& j, Person& p) { j.at("name").get_to(p.name); j.at("age").get_to(p.age); j.at("isStu").get_to(p.isStu); }
当然,如果+=stdc++11及其以上,我们还能使用宏来做
像这样:
我们跳转到库函数来看看
/*! @brief macro @def NLOHMANN_DEFINE_TYPE_INTRUSIVE @since version 3.9.0 */ #define NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type, ...) \ friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }
我们使用宏就可以这样来写代码
#include using json = nlohmann::json; using namespace std; // 结构体定义 struct Person { string name; int age; bool isStu; NLOHMANN_DEFINE_TYPE_INTRUSIVE(Person, name, age, isStu) };
上述是在我们包含了json库的情况下可以那么去做
当然,为了代码可以更加灵活 我们可以自己来实现序列化和反序列化函数
见下:
// 手动实现c++对象与JSON字符串的转换 #include #include using namespace std; class Person { private: string name; int age; bool isStu; public: Person(const string& name, int age, bool isStu) : name(name), age(age), isStu(isStu) {} Person() {} ~Person() {} // 序列化函数 string serialize() { string ans = "{\"name\":\"" + name + "\", \"age\":" + std::to_string(age) + ", \"isStudent\":" + std::to_string(isStu) + "}"; return ans; } // 反序列化函数 static Person deserialize(const string& json) { // 解析JSON字符串 填充Person对象 Person p; size_t nameStart = json.find("\"name\":\"") + 8; size_t nameEnd = json.find("\"", nameStart); p.name = json.substr(nameStart, nameEnd - nameStart); size_t ageStart = json.find("\"age\":") + 6; size_t ageEnd = json.find(",", ageStart); p.age = stoi(json.substr(ageStart, ageEnd - ageStart)); size_t isStuStart = json.find("\"isStu\":") + 8; p.isStu = (json.substr(isStuStart, 4) == "true"); return p; } }; int main(int argc, char const *argv[]) { Person p = {"Kyrie Irving", 30, false}; string serialized = p.serialize(); Person pDeserialized = Person::deserialize(serialized); return 0; }
对比分析 转换效率与资源消耗分析
手动实现
转换效率
- 优点:手动实现可以针对特定的应用场景进行高度优化,避免了不必要的函数调用和数据复制。在处理简单的 JSON 结构时,手动实现的代码可以非常高效,因为它直接操作数据,没有额外的开销。
- 缺点:手动实现的代码通常缺乏通用性,对于复杂的 JSON 结构,编写和维护代码的难度会大大增加。而且,手动实现很难处理一些复杂的情况,如嵌套的 JSON 对象和数组,这可能会导致代码的效率下降。
资源消耗
- 优点:手动实现可以精确控制内存的使用,避免了第三方库可能带来的额外内存开销。在资源受限的环境中,手动实现可能是更好的选择。
- 缺点:手动实现需要开发者自己处理各种边界情况和错误处理,这可能会增加代码的复杂度和内存泄漏的风险。而且,手动实现的代码通常不够健壮,容易出现错误。
第三方库
转换效率
- 优点:第三方库通常经过了高度优化,具有较高的转换效率。它们使用了各种算法和数据结构来提高序列化和反序列化的速度,特别是在处理复杂的 JSON 结构时,第三方库的优势更加明显。
- 缺点:第三方库可能会有一些额外的开销,如内存分配和函数调用。在处理简单的 JSON 结构时,这些开销可能会显得比较明显。
资源消耗
- 优点:第三方库通常会处理各种边界情况和错误处理,代码更加健壮,减少了内存泄漏的风险。而且,一些库还提供了内存池等机制来优化内存使用。
- 缺点:第三方库可能会引入额外的依赖和开销,增加了可执行文件的大小。在资源受限的环境中,这可能是一个问题。
转换效率与资源消耗分析工具
使用性能分析工具(如gprof、Valgrind等)来监测程序的 CPU 使用情况和内存分配情况。
显然,我们发现,手动实现,更需要我们注意错误处理机制
错误处理机制
JSON解析错误通常发生在JSON格式不正确时,比如缺少闭合的括号、逗号使用错误、或键值对未正确配对等。在C++中解析JSON字符串,当遇到错误时,需要具体分析错误类型,以便采取相应的处理措施。
我们来看看 nlohmann/json 这样的库是如何解决的,学习 一下其解析错误处理的方式
我们来看看这个库中的 parse_error 异常 的实现
我们来解析一下该模板函数
模板参数 Exception
该函数是一个模板函数,允许接收不同类型的异常对象(如 parse_error 或 exception),体现了库的灵活性和对多种异常类型的支持。
参数说明
- std::size_t:通常表示解析错误的位置(如行号或列号),但此处被注释忽略(可能由其他机制处理)。
- const std::string&:错误描述信息,同样被注释忽略,实际错误信息通过异常对象 ex 传递。
- const Exception& ex:具体的异常对象,包含详细的错误信息(如错误类型、位置等)。
核心逻辑分析
标记错误状态
errored = true;
设置布尔变量 errored 为 true,用于记录解析过程中发生了错误。此变量可能在后续流程中用于判断是否需要终止解析或回滚操作。
异常对象的处理
static_cast(ex);
此操作仅为避免编译器对未使用变量 ex 的警告,无实际功能影响。
异常抛出条件
if (allow_exceptions)
{
JSON_THROW(ex);
}
- allow_exceptions 是库的全局配置标志,控制是否允许抛出异常。若为 true,则通过宏 JSON_THROW 抛出异常 ex。
- JSON_THROW 是库定义的宏,通常封装了 throw 语句,用于统一异常抛出行为。
返回值
return false;
返回 false 表示解析失败,通知上层解析器终止当前解析流程。
错误处理分层
- 异常模式:默认情况下(allow_exceptions=true),直接抛出异常,适合需要即时终止解析的场景(如配置解析失败需立即报错)。
- 静默模式:若关闭异常(allow_exceptions=false),仅记录错误状态,适合需要容错处理的场景(如日志记录或流式解析)。
异常信息传递
通过异常对象 ex 传递具体错误信息(如 parse_error::create(101, ...)),包含错误代码、位置和描述,便于开发者定位问题。例如,用户遇到的空输入错误会触发 [json.exception.parse_error.101],明确提示问题原因。
与解析流程的集成
此函数通常由词法分析器(lexer)或语法分析器(parser)在检测到非法 Token 或结构时调用,构成完整的错误处理链路。
与其他机制的协作
assert_invariant 检查
在解析过程中,库通过 assert_invariant 函数校验 JSON 对象状态(如指针非空),确保错误不会进一步传播到不一致的状态。
错误日志记录
用户可通过自定义回调函数或捕获异常后记录日志(如 log_error 函数),实现错误追踪。
依托于 nlohmann/json 库 我们可以处理多种异常错误
错误类型及检测
#include #include #include using json = nlohmann::json; using namespace std; // 日志记录函数 void log_error(const string& message, int error_code = 0) { cerr {"name", utf8_name}, {"price", p.price}}; } int main() { Product product{"IPhone", 4999}; // GBK存储 try { json j = product; cout