C++开源库nlohmann/json的介绍和使用详解
作者:yuanhao
前言
该库的地址:github.com/nlohmann/json,到目前已经拥有38.5K的star,可以说是被大家广泛赞同,经过简单探究,确实发现非常不错,可以在项目中使用。
该库的集成非常容易,只有1个hpp
文件,拿来即用,完全不需要任何复杂的编译,尤其是刚入门的C++开发来说,编译一些开源库是永远的噩梦。
同时该库使用C++11代码编译,使用了不少特性,也是值得学习其原理,学习C++的不错选择。
下面安装其readme
,简单介绍,以及自己的使用体验。
1. JSON as first-class data type
中文翻译为json作为一等数据类型,即我们可以将json数据类型作为一等公民(first-class citizen
),并且支持相应的操作和语法。
关于某种语言是否把某个特性或者功能作为一等公民看待,我个人有2点感受比较深刻。第一个是Java到Kotlin的编程语言变化,同为JVM语言,Kotlin把函数和lambda表达式看成一等公民,在Kotlin中可以将函数作为参数、作为返回值等,在Kotlin官方实例和源码中大量使用,极大地提高代码简洁性。第二个是学习C++时,C++没有把原生数组类型看成一等公民,比如数组不支持直接拷贝赋值,原因很多,比如必须固定长度,缺乏高级操作以及语法糖支持,指针语义容易混淆(C++中,数组名实际上是一个指向数组首元素的指针)等,所以推荐使用std::vector
来替代原生数组。
在C++中并没有直接将json看成一等公民,与此对应的Python就有非常好的支持,而该库想实现这一目的,就必须得提供足够简洁的使用方式,以及足够丰富的操作。比如我想创建一个json对象,如下:
//想创建的json对象,标准json格式 { "pi": 3.141, "happy": true, "name": "Niels", "nothing": null, "answer": { "everything": 42 }, "list": [1, 0, 2], "object": { "currency": "USD", "value": 42.99 } } //代码如下 //创建一个空对象 json j; //增加一个数字,使用double类型存储, j["pi"] = 3.141; //增加一个布尔值,使用bool类型存储 j["happy"] = true; //增加一个字符串,使用std::string类型存储 j["name"] = "Niels"; //通过传递一个nullptr增加一个空对象 j["nothing"] = nullptr; //在对象内部增加一个对象 j["answer"]["everything"] = 42; //增加一个数组,使用std::vector j["list"] = {1, 0, 2}; //增加另一个对象 j["object"] = { {"currency", "USD"}, {"value", 42.99}}; //或者使用更方便的方式 json j2 = { {"pi", 3.141}, {"happy", true}, {"name", "Niels"}, {"nothing", nullptr}, {"answer", { {"everything", 42} }}, {"list", {1, 0, 2}}, {"object", { {"currency", "USD"}, {"value", 42.99} }} }; std::cout << j2.dump(4) << std::endl; //运行结果 { "answer": { "everything": 42 }, "happy": true, "list": [ 1, 0, 2 ], "name": "Niels", "nothing": null, "object": { "currency": "USD", "value": 42.99 }, "pi": 3.141 }
这里看似非常简单,但是有一些原理我们是需要知道的。我们对比一下给定的json
中list
的结构,它是一个数组,使用[1, 0, 2]
,我们在j
和j2
的构造中,都是使用的{}
来进行赋值的,这一点就和把json看成一等公民的Python语言有较大区别,下面是Python代码:
import sys import os import json # 定义一个 Python 对象 person = { "name": "Alice", "age": 25, "hobby": ["reading", "music"] } # 将 Python 对象转换为 json 字符串 json_str = json.dumps(person) # 输出转换后的 JSON 字符串 print(json_str) //输出结果 {"name": "Alice", "age": 25, "hobby": ["reading", "music"]}
可以发现在Python构建对象时,对于数组结构,可以直接使用[]
,更符合常识,C++为什么不可以,因为j["list"] = {1, 0, 2};
是通过重载[]
运算符对j
进行赋值,默认的数据成员对象类型是std::vector
,对std::vector
的初始化使用列表初始化时,不能使用[]
,即如下:
//C++11可以使用列表初始化 std::vector list = {0, 1, 2}; //错误 std::vector list1 = [0, 1, 2];
因为该库只能利用重载运算符等方式来让json操作看起来像是操作json,而非C++语言支持这种操作。
搞清楚这个之后,我们再来看看object
,在原始的json
中,我们可以清晰地知道它对应的类型是一个类类型:
"object": { "currency": "USD", "value": 42.99 }
但是在使用对j
的object
的赋值如下:
j["object"] = { {"currency", "USD"}, {"value", 42.99}};
这不是和前面所说的数组赋值冲突了吗,依据列表初始化规则,这个完全可以解析为数组类型,即[["currency", "USD"],["value", 42.99]]
,也就是二维数组,这时我们又可以对比一下Python可以怎么写:
import sys import os import json # 定义一个 Python 对象 person = { "name": "Alice", "age": 25, "object": { "currency":"USD", "value":"42.99" } } # 将 Python 对象转换为 JSON 字符串 json_str = json.dumps(person) # 输出转换后的 JSON 字符串 print(json_str) #输出结果 {"name": "Alice", "age": 25, "object": {"currency": "USD", "value": "42.99"}}
可以发现在Python中可以使用:
,这样对应的键和值非常容易识别,那C++为什么不可以呢?还是一样的问题,在代码j["object"] = { {"currency", "USD"}, {"value", 42.99}};
中,是使用std::map
数据结构来作为其默认的成员数据类型,而C++11中可以使用列表初始化来初始化std::map
,同时没有:
的语法:
//C++11可以使用列表初始化 std::map map = { {"currency", "USD"}, {"value", 42.99} }; //错误 std::map map1 = { {"currency":"USD"}, {"value":42.99} };
搞明白为什么之后,我们就要回答前面的问题,为什么object
没有被解析为std::vector
类型呢?原因是默认规则就是这样的,即一层和2层{}
的默认处理逻辑是不一样的。
假如有一些极限情况,我就是想用数组形式来保存对象格式,这时可以显示地声明json值的类型,使用json::array()
和json::object()
函数:
//C++11可以使用列表初始化 std::map map = { {"currency", "USD"}, {"value", 42.99} }; //错误 std::map map1 = { {"currency":"USD"}, {"value":42.99} };
搞明白为什么之后,我们就要回答前面的问题,为什么object
没有被解析为std::vector
类型呢?原因是默认规则就是这样的,即一层和2层{}
的默认处理逻辑是不一样的。
假如有一些极限情况,我就是想用数组形式来保存对象格式,这时可以显示地声明json值的类型,使用json::array()
和json::object()
函数:
//显示声明是一个数组,而非对象 json empty_array_explicit = json::array(); //对于没有构造函数或者这种,默认隐式类型是对象 json empty_object_implicit = json({}); json empty_object_explicit = json::object(); //是一个数组,而非对象 json array_not_object = json::array({ {"currency", "USD"}, {"value", 42.99} }); std::cout << empty_array_explicit.dump(4) << std::endl; std::cout << empty_object_implicit.dump(4) << std::endl; std::cout << empty_object_explicit.dump(4) << std::endl; std::cout << array_not_object.dump(4) << std::endl; //运行结果 [] {} {} [ [ "currency", "USD" ], [ "value", 42.99 ] ]
搞清楚默认类型,以及如何显示声明非常重要。
2. 序列化/反序列化
既然想把json打造为一等公民,序列化和反序列化是必须要具备 ,而且要可以从不同的源来进行序列化/反序列化,比如从文件、字符串等。
2.1 与字符串
我们可以从字符串字面量来创建一个json
对象,注意和上面使用=
进行列表初始化的方式不同,这里的参数是字符串字面量:
json j = "{"happy":true,"pi":3.141}"_json; std::cout << j.dump(4) << std::endl; auto j2 = R"({ "happy": true, "pi": 3.141 })"_json; std::cout << j2.dump(4) << std::endl; json j1 = "{"happy":true,"pi":3.141}"; std::cout << j1.dump(4) << std::endl; //运行结果 { "happy": true, "pi": 3.141 } { "happy": true, "pi": 3.141 } "{"happy":true,"pi":3.141}"
上面代码都是通过字符串初始化一个json
对象,但是需要注意的点很多。
2.1.1 原始字符串字面量(Raw String Literal)
首先就是j2
的初始化,它使用了R"()"
这种语法,这种表示字符串的方式叫做原始字符串字面量。在C++中,可以使用R"()"
来表示原始字符串字面量,使用原始字符串字面量可以方便地包含特殊字符(比如反斜杠、引号等)或者多行文本的字符串,而无需对这些特殊字符进行转移。
比如如下代码:
{ qDebug() << "Hello\tWorld!"; qDebug() << R"(Hello\tWorld NEW LINE)"; }
运行结果是:
可以发现普通字符串中\t
被解释为制表符,而在原始字符串字面量中,\t
不会被解析,并且换行也被保留了下来。其实在其他语言中,使用这种方式更为简单,比如Kotlin使用"""
进行包裹字符串,或者Python使用```进行包裹字符串。
通过这个小知识点的学习,我们明显发现j2
就比j
的初始化方式更人性化,至少不用去在意字符串中的转移字符。
2.1.2 自定义字面量操作符
然后我们关注点来到j
和j1
的区别,可以发现j
多了一个_json
的后缀,然后在输出打印中就可以正常解析,而j1
却不可以。这里的核心点就是字符串字面量后面的_json
,我们看一下它的源码:
JSON_HEDLEY_NON_NULL(1) inline nlohmann::json operator "" _json(const char* s, std::size_t n) { return nlohmann::json::parse(s, s + n); }
看到operator
就应该想到自定义操作符,没错,operator ""
是C++11引入的自定义字面量操作符,通过重载operator ""
可以为特定的后缀自定义语义,这种机制可以使得我们可以像使用内置的字面量(如整数、浮点数、字符串等)一样自然地使用自定义的字面量,从而提高代码的可读性和表达力。
举个简单的例子:
//为long long类型定义一个_km后缀 constexpr long long operator "" _km(unsigned long long x) { return x * 1000; } long long distance = 20_km; std::cout << "distance:" << distance << "meters\n"; //输出结果 distance:20000meters
从这个例子我们来看上面_json
的含义,它就是给const char*
类型即字符串类型添加一个_json
后缀,作用就是调用nlohmann::json::parse(s, s + n)
返回一个json
对象,理解了这个之后,我们也就可以理解为什么j1
不能被解析,因为没有_json
后缀,根本不会调用parse
函数进行解析成json
对象。
因为_json
这种用法就是语法糖,所以其实想解析一个字符串成json
,就可以直接调用json::parse()
函数:
json j3 = json::parse(R"({"happy": true,"pi": 3.141})");
从前面的错误用法打印来看,一般情况下我们认为json
对象要不表示一个对象,要不表示一个数组,但是从打印j1
来看:
json j1 = "{"happy":true,"pi":3.141}"; std::cout << j1.dump(4) << std::endl; //运行结果 "{"happy":true,"pi":3.141}"
这里居然返回一个字符串。所以这里一定要区分序列化和赋值的区别,对于序列化,在前面我们说了2种方式,一种是使用列表初始化,一种是使用字符串字面量,而直接赋值的话,json
内部会直接保存一个字符串。
2.2 与file
从文件中反序列化一个json
对象可以说是在配置文件时非常常用了,这里使用也非常简单,代码如下:
std::string dirPath = QApplication::applicationDirPath().toStdString(); std::string file = dirPath + "/file.json"; //使用ifstream读取file std::ifstream i(file); json j_file; //把输入流中的信息放入json中 i >> j_file; std::cout << j_file.dump(4) << std::endl; json j_file_bak = { {"pi", 3.141}, {"happy", true}, {"name", "Niels"}, {"nothing", nullptr}, { "answer", { {"everything", 42} } }, {"list", {1, 0, 2}}, { "object", { {"currency", "USD"}, {"value", 42.99} } } }; std::string file_bak = dirPath + "/file_bak.json"; std::ofstream o(file_bak); o << std::setw(4) << j_file_bak << std::endl;
可以发现只要获取到标准的输入输出流之后,我们就可以使用>>
和<<
符号来进行文件序列化和反序列化了,这里的原理非常简单,也是操作符重载,在源码中重载了>>
和<<
:
friend std::istream& operator>>(std::istream& i, basic_json& j) { parser(detail::input_adapter(i)).parse(false, j); return i; } friend std::ostream& operator<<(std::ostream& o, const basic_json& j) { // read width member and use it as indentation parameter if nonzero const bool pretty_print = o.width() > 0; const auto indentation = pretty_print ? o.width() : 0; // reset width to 0 for subsequent calls to this stream o.width(0); // do the actual serialization serializer s(detail::output_adapter<char>(o), o.fill()); s.dump(j, pretty_print, false, static_cast<unsigned int>(indentation)); return o; }
对于C++开发来说,左移和右移符号已经非常熟悉了。
3. STL-like access
STL-like access
是STL风格访问的意思,在C++中可以为一些类定义STL风格访问的API,可以提高类型的灵活性和易用性,这样我们就可以像STL容器一样使用迭代器、算法等功能,从而可以简化很多操作,提高了代码的可读性和可维护性。
3.1 STL风格API
由于json
本身就是由标准库中的类型进行解析的,所以为其设计一套STL风格访问的API也就很有必要,使用如下:
//创建一个数组, 使用push_back json j; j.push_back("foo"); j.push_back(1); j.push_back(true); //可以使用emplace_back j.emplace_back(1.78); //使用迭代器遍历数组 for (json::iterator it = j.begin(); it != j.end(); it++) { std::cout << *it << std::endl; } //快速for循环 for (auto& element : j) { std::cout << element << std::endl; } //getter/setter const auto tmp = j[0].template get<std::string>(); j[1] = 42; bool foo = j.at(2); //比较运算符 j == R"(["foo", 1, true, 1.78])"_json; j.size(); j.empty(); j.type(); j.clear(); //快捷类型判断 j.is_null(); j.is_boolean(); j.is_number(); j.is_object(); j.is_array(); j.is_string(); //创建一个对象 json o; o["foo"] = 23; o["bar"] = false; o["baz"] = 3.14; //特殊的成员迭代器函数 for (json::iterator it = o.begin(); it != o.end(); it++) { std::cout << it.key() << " : " << it.value() << std::endl; } //快速for循环 for (auto& el : o.items()) { std::cout << el.key() << " : " << el.value() << std::endl; } //c++17特性,结构化绑定 for (auto& [key, value] : o.items()) { std::cout << key << " : " << value << std::endl; } //判断有没有某个键值对 if (o.contains("foo")) { std::cout << "contain foo" << std::endl; }
这种使用方式可以让我们操作json像操作标准容器一样,非常方便。
3.2 从STL容器构造
同理,我们可以从STL容器构造出json对象,任何序列容器,比如std::array
、std::vector
、std::deque
、std::forward_list
和std::list
,其中保存值可以构造json的值,比如int
,float
、boolean
、字符串类型等,这些容器均可以用来构造json数组。对于类似的关联容器(std::set
、std::multiset
、std::unordered_set
、std::unordered_multiset
)也是一样,但是在这种情况下,数组元素的顺序取决于元素在数组中的排序方式。
使用代码如下:
std::vector<int> c_vector {1, 2, 3, 4}; json j_vec(c_vector); std::deque<double> c_deque {1,2, 2.3, 3.4, 5.6}; json j_deque(c_deque); std::list<bool> c_list {true, true, false, true}; json j_list(c_list); std::forward_list<int64_t> c_flist {12345678909876, 23456789098765, 34567890987654, 45678909876543}; json j_flist(c_flist); std::array<unsigned long, 4> c_array{{1, 2, 3, 4}}; json j_array(c_array); std::set<std::string> c_set {"one", "two", "three", "four", "one"}; json j_set(c_set); std::unordered_set<std::string> c_uset {"one", "two", "three", "four", "one"}; json j_uset(c_uset); std::multiset<std::string> c_mset {"one", "two", "one", "four"}; json j_mset(c_mset); std::unordered_multiset<std::string> c_umset {"one", "two", "one", "four"}; json j_umset(c_umset);
关于这几种STL容器,可以做个简单的概述:
容器类型 | 底层实现 | 特点 | 适用场景 |
---|---|---|---|
std::vector | 动态数组 | 可变大小的连续存储空间,支持随机访问,尾部插入和删除效率高。 | 需要在末尾进行频繁插入和删除操作,以及随机访问元素的情况。 |
std::array | 静态数组 | 固定大小的连续的静态数组,在编译期就确定了大小,并且不会改变。 | 替代原生数组,有更多的成员函数与操作API。 |
std::list | 双向链表 | 支持双向迭代器,插入和删除元素效率高,不支持随机访问。 | 需要频繁在中间位置进行插入和删除操作,不需要随机访问元素。 |
std::deque | 双端队列 | 支持随机访问,支持在两端插入和删除元素,动态地分配存储空间。 | 需要在两端插入和删除元素,并且需要随机访问元素的情况。 |
std::set | 红黑树 | 自动排序元素,不允许重复元素,插入和删除的时间复杂度为O(logN)。 | 需要自动排序且不允许重复元素的情况。 |
std::multiset | 红黑树 | 自动排序元素,允许重复元素,插入和删除的时间复杂度为O(1)。 | 需要自动排序且允许重复元素的情况。 |
std::unordered_set | 哈希表 | 无序存储元素,不允许重复元素,插入和查找的时间复杂度为O(1)。 | 不需要排序,并且不允许重复元素的情况。 |
std::unordered_multiset | 哈希表 | 无序存储元素,允许重复元素,插入和查找的时间复杂度为O(1)。 | 不需要排序,并且允许重复元素的情况。 |
类似的,STL的键值对容器,只要键可以构造std::string
对象,值可以构造json对象的,也可以用来构造json对象类型,测试代码如下:
std::map<std::string, int> c_map { {"one", 1}, {"two", 2}, {"three", 3} }; json j_map(c_map); std::unordered_map<const char*, double> c_umap { {"one", 1.2}, {"two", 2.3}, {"three", 3.4} }; json j_umap(c_umap); std::multimap<std::string, bool> c_mmap { {"one", true}, {"two", false}, {"three", false}, {"three", true} }; json j_mmap(c_mmap); std::unordered_multimap<std::string, bool> c_ummap { {"one", true}, {"two", true}, {"three", false}, {"three", true} }; json j_ummap(c_ummap);
这几种容器也做个概述,和std::set
类似,也是从是否自动排序和重复(一键多值)这两个维度来扩展,和Java还是有一点区别的,在Java中使用最多的是HashMap
,类似C++
的std::unordered_set
:
容器类型 | 底层 | 特点 |
---|---|---|
std::map | 红黑树 | 根据key进行自动排序;每个key只能出现一次;由于红黑树的平衡性,查找、插入和删除的时间复杂度均为O(logn);不支持高效的随机访问。 |
std::unordered_map | 哈希表 | 不会自动排序;每个key只能出现一次;查找、插入和删除的时间复杂度为O(1),支持高效的随机访问。 |
std::multimap | 红黑树 | 根据key自动排序;每个key支持对应多个value,即可以插入多个key一样的键值对;查找、插入和删除的时间复杂度为O(logn)。 |
std::unorder_multimap | 哈希表 | 不会自动排序;支持每个key对应对个value;操作的时间复杂度为O(1)。 |
4. JSON指针和补丁
该库还支持JSON指针,是一种作为寻址结构化值的替代方法。并且,JSON补丁(Patch)允许描述2个JSON值之间的差异。这两点我觉得非常不错,有助于在版本迭代时进行合并配置文件,直接看代码:
//原始json对象 json j_1 = R"({ "baz": ["one", "two", "three"], "foo": "bar" })"_json; std::cout << "j_1:" << j_1.dump(4) << std::endl; std::cout << j_1["/baz/1"_json_pointer] << std::endl; //补丁也是一个json对象 json j_patch = R"([ { "op": "replace", "path": "/baz", "value": "boo" }, { "op": "add", "path": "/hello", "value": ["world"] }, { "op": "remove", "path": "/foo"} ])"_json; std::cout << "j_patch:" << j_patch.dump(4) << std::endl; //合并补丁 json j_result = j_1.patch(j_patch); std::cout << "j_result:" << j_result.dump(4) << std::endl; //计算出差值补丁,差值是第一个参数如何操作成为第二个参数的差值 json j_diff = json::diff(j_result, j_1); std::cout << "j_diff:" << j_diff.dump(4) << std::endl; //使用插值进行合并 json j_result_1 = j_result.patch(j_diff); std::cout << "j_result_1:" << j_result_1.dump(4) << std::endl; //输出结果 j_1:{ "baz": [ "one", "two", "three" ], "foo": "bar" } "two" j_patch:[ { "op": "replace", "path": "/baz", "value": "boo" }, { "op": "add", "path": "/hello", "value": [ "world" ] }, { "op": "remove", "path": "/foo" } ] j_result:{ "baz": "boo", "hello": [ "world" ] } j_diff:[ { "op": "replace", "path": "/baz", "value": [ "one", "two", "three" ] }, { "op": "remove", "path": "/hello" }, { "op": "add", "path": "/foo", "value": "bar" } ] j_result_1:{ "baz": [ "one", "two", "three" ], "foo": "bar" }
上面代码很容易理解,首先就是json指针的使用j_1["/baz/1"_json_pointer]
,在这里和前面_json
的自定义操作符一样,_json_pointer
也是对字符串的自定义操作,其中通过/
符号来找到json对象中深层次的内容。
接着就是补丁,补丁自己也是一个json对象,每个操作对应一个对象,分别是op
表示操作,path
是json指针,表示需要操作的地方,value
就是新的值。调用json::patch
方法就可以把补丁合并,生成一个新的json对象,可以看上面例子中的j_result
对象。
最后就是json::diff
方法,它是用来计算2个json对象的差值,生成补丁。传递进该函数的2个json对象,补丁就是第一个json对象到第二个json对象的补丁,所以在上面例子中,我们对j_result
合并j_diff
补丁,又可以回到最开始的json对象。
或许你可能觉得json指针有点太难用了,在前面我们也看见了,其实patch
也是一个json对象,所以该库还支持直接使用json对象来进行合并补丁,这种场景非常适合配置文件的迭代,比如下面代码:
//多了一个配置项head,已有的配置项的值为默认值 json j_new_config = { {"name", "modbus"}, {"config",{ {"type", ""}, {"startIndex", 0}} }, {"head", 10} }; std::cout << "j_new_config:" << j_new_config.dump(4) << std::endl; //旧的配置项,已经有值了 json j_old_config = { {"name", "modbus"}, {"config", { {"type", "floatlh"}, {"startIndex", 17}} } }; std::cout << "j_old_config:" << j_old_config.dump(4) << std::endl; j_new_config.merge_patch(j_old_config); std::cout << "result:" << j_new_config.dump(4) << std::endl; //输出结果 j_new_config:{ "config": { "startIndex": 0, "type": "" }, "head": 10, "name": "modbus" } j_old_config:{ "config": { "startIndex": 17, "type": "floatlh" }, "name": "modbus" } result:{ "config": { "startIndex": 17, "type": "floatlh" }, "head": 10, "name": "modbus" }
在上面代码中,假如j_old_config
是已经运行的配置项,而j_new_config
是这次版本升级后的新的配置项,其中多了一个字段head
,且其他配置项都是默认值,经过把旧的配置文件合并到新的配置文件中,我们可以看到最终合并后的配置文件,即含有head
字段,也有旧的配置,这样就完成了配置文件的升级。
5. 任意类型转换
在前面说过,对于支持的类型可以隐式的转换为json中的值,但是当从json值获取值时,不建议使用隐式转换,建议使用显示的方式。比如下面代码:
//推荐写法 std::string s1 = "Hello World"; json js = s1; auto s2 = js.template get<std::string>(); //不推荐写法 std::string s3 = js;
这里有一个写法是template get<std::string>()
,其实也就是模板成员函数调用的写法。
5.1 直接写法
我们研究json序列化库的最终目的是想把任何类型都可以进行序列化和反序列化,通过前面的学习,我们可以大概知道如何把任意一个类类型转成json对象,以及从json对象转变为类类型对象。直接看代码:
Student s = {"jack", 18}; //类类型对象转换为json对象 json j; j["name"] = s.name; j["age"] = s.age; //json对象转换为类类型对象 Student s1 {j["name"].template get<std::string>(), j["age"].template get<int>()};
这里我们定义一个Student
类型,通过前面所学的json操作,很容易写出这样代码,但是这种代码有点冗余。
5.2 from_json和to_json
其实我们可以把序列化和反序列化的操作写在类中,也就是让该类拥有了该能力,这种写法如下:
#ifndef STUDENT_H #define STUDENT_H #include <string> #include "json.hpp" #include <iostream> using json = nlohmann::json; struct Student { std::string name; int age; }; void to_json(json& j, const Student& s) { j = json{ {"name", s.name}, {"age", s.age} }; } void from_json(const json& j, Student& s) { j.at("name").get_to(s.name); j.at("age").get_to(s.age); } #endif // STUDENT_H
直接在定义类的头文件中多定义2个方法,分别为to_json
和from_json
,然后使用如下:
Student s = {"jack", 18}; //类类型对象转换为json对象 json j = s; std::cout << "j:" << j.dump(4) << std::endl; //json对象转换为类类型对象 auto s2 = j.template get<Student>();
这里也是非常容易理解,当调用json的构造函数,参数是自定义类型时,就会调用to_json
方法;类似的,当调用template get<Type>()
或者get_to(Type)
时,这个from_json
就会被调用。这里有几点需要注意:
- 这些方法必须是公有的命名空间或者类型的命令空间,否则库无法定位它们。
- 这些方法必须是可访问的,不能是私有的等。
- 函数参数必须要注意,从上面例子可以看出,否则无法自动定位它们。
- 自定义的类型必须有且可以默认构造。
我们仔细思考一下这2个方法,其中to_json
是根据类对象构造json对象,在前面我们说了很多。但是from_json
可能就会有问题,比如json对象中缺少一些key,这时就会报错,因为访问不到,比如下面代码:
json j = { {"name", "jack"} }; //json对象转换为类类型对象 auto s2 = j.template get<Student>();
这个j对象就没有age,然后调用from_json
时就会出错,这里会直接抛出异常,有没有其他办法不抛出异常呢?还是有的,可以通过value
方法进行:
void from_json(const json& j, Student& s) { s.name = j.value("name", ""); s.age = j.value("age", 0); }
当json对象中没有某个键时,可以通过该方法设置一个默认值。
5.3 使用宏
上面代码可能还不够简洁,这里可以更加容易,如果想序列化后的字段和原来类类型字段一样,可以使用宏来默认实现,这里有2个宏,一个是NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE
用于class或者struct对象其成员都是public的情况,还可以使用有侵入式的NLOHMANN_DEFINE_TYPE_INTRUSIVE
来访问private成员。使用宏的话,整个使用就非常简单了,测试代码如下:
struct Student { std::string name; int age; NLOHMANN_DEFINE_TYPE_INTRUSIVE(Student, name, age) }; struct Teacher { std::vector<Student> students; std::string name; std::string subject; NLOHMANN_DEFINE_TYPE_INTRUSIVE(Teacher, students, name, subject) }; //使用 Student s1 {"zs", 11}; Student s2 {"ls", 13}; Teacher t; t.name = "wang"; t.subject = "math"; t.students.push_back(s1); t.students.push_back(s2); json j = t; std::cout << "j:" << j.dump(4) << std::endl; Teacher t1 = j.template get<Teacher>();
只需要在宏里面写好类名,以及需要序列化的成员即可。
5.4 枚举类
在自定义类型的序列化中,枚举类型需要额外关注,默认情况下枚举会被序列化为int值,因为枚举本身保存的也就是int值。但是,在序列化和反序列化中,这种逻辑可能出现问题。比如现在有枚举类如下:
enum TaskState{ TS_STOPPED, //0 TS_RUNNING, //1 TS_COMPLETED, //2 TS_INVALID = -1 };
这里我们把TS_INVALID赋值为-1,表示无效,其他枚举值会按照规范依次被赋值为0、1和2,不论我们打印还是默认序列化,TS_STOPPED的值都是0:
std::cout << TS_STOPPED << std::endl; //输出结果 0
假如后面项目变化,需要新增一种枚举,如下:
enum TaskState{ TS_TEMP, //0 TS_STOPPED, //1 TS_RUNNING, //2 TS_COMPLETED, //3 TS_INVALID = -1 };
这时TS_TEMP就会变成0,假如我们有一个旧对象序列化后的json保存在文件里,旧的json中保存的还是0,经过反序列化后0会被反序列化为TS_TEMP,而不是预期的TS_STOPPED了,这就是默认使用int作为枚举值的弊端。
在该库中,我们可以更加精确地指定给定枚举如何映射到json以及如何从json映射,使用NLOHMANN_JSON_SERIALIZE_ENUM
宏,代码如下:
enum TaskState{ TS_STOPPED, //0 TS_RUNNING, //1 TS_COMPLETED, //2 TS_INVALID = -1 }; NLOHMANN_JSON_SERIALIZE_ENUM(TaskState, { {TS_INVALID, nullptr}, {TS_STOPPED, "stopped"}, {TS_RUNNING, "running"}, {TS_COMPLETED, "completed"} })
该宏就是可以声明在to_json()
和from_json()
时枚举所对应的字符串,这样不使用默认的int来保存,就大大提高了程序的稳定性:
json j = TS_STOPPED; assert(j == "stopped"); json j3 = "running"; assert(j3.template get<TaskState>() == TS_RUNNING);
上述代码可以正常运行,说明TS_RUNNING
在序列化时变成了running
,假如我们新增了一种枚举,只要使用宏包括进来:
enum TaskState{ TS_TEMP, TS_STOPPED, //0 TS_RUNNING, //1 TS_COMPLETED, //2 TS_INVALID = -1 }; NLOHMANN_JSON_SERIALIZE_ENUM(TaskState, { {TS_INVALID, nullptr}, {TS_STOPPED, "stopped"}, {TS_RUNNING, "running"}, {TS_COMPLETED, "completed"}, {TS_TEMP, "temp"} })
上述代码依旧可以执行成功,不会出现反序列化错误的情况。
这里有一点需要特别注意,就是在宏NLOHMANN_JSON_SERIALIZE_ENUM
的定义中,我们把默认的无效枚举TS_INVALID定义在第一个,这个是有特殊意义的,假如json中的值未定义,无法反序列化为任何一种枚举,就会被反序列化为这个默认值,代码如下:
json jPi = 3.14; assert(jPi.template get<TaskState>() == TS_INVALID);
上面的3.14
属于未定义的枚举值,在这种情况下会默认反序列化为默认值。
以上就是C++开源库nlohmann/json的介绍和使用详解的详细内容,更多关于C++ nlohmann/json的资料请关注脚本之家其它相关文章!