04 - 用json序列化
尝试从零实现(抄)一个反射系统,并应用于我酝酿中的游戏引擎项目里!本节将尝试使用nlohmann/json
实现序列化和反序列化。
Json库简介
创建json对象
读json文件
可以通过读取一个JSON文件来创建一个json
对象:
#include <fstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
// ...
std::ifstream f("example.json");
json data = json::parse(f);
读json字面量
可以通过原始字符串 + json::parse()
,用户定义的原始字符串,或初始化列表来创建一个json
对象:
// Using (raw) string literals and json::parse
json ex1 = json::parse(R"(
{
"pi": 3.141,
"happy": true
}
)");
// Using user-defined (raw) string literals
using namespace nlohmann::literals;
json ex2 = R"(
{
"pi": 3.141,
"happy": true
}
)"_json;
// Using initializer lists
json ex3 = {
{"happy", true},
{"pi", 3.141},
};
使用json类
还可用通过json
类提供的operator[]
等方式将数据写入json
类中。例如想要创建如下JSON对象:
{
"pi": 3.141,
"happy": true,
"name": "Niels",
"nothing": null,
"answer": {
"everything": 42
},
"list": [1, 0, 2],
"object": {
"currency": "USD",
"value": 42.99
}
}
可以这样做:
// create an empty structure (null)
json j;
// add a number that is stored as double (note the implicit conversion of j to an object)
j["pi"] = 3.141;
// add a Boolean that is stored as bool
j["happy"] = true;
// add a string that is stored as std::string
j["name"] = "Niels";
// add another null object by passing nullptr
j["nothing"] = nullptr;
// add an object inside the object
j["answer"]["everything"] = 42;
// add an array that is stored as std::vector (using an initializer list)
j["list"] = { 1, 0, 2 };
// add another object (using an initializer list of pairs)
j["object"] = { {"currency", "USD"}, {"value", 42.99} };
// instead, you could also write (which looks very similar to the JSON above)
json j2 = {
{"pi", 3.141},
{"happy", true},
{"name", "Niels"},
{"nothing", nullptr},
{"answer", {
{"everything", 42}
}},
{"list", {1, 0, 2}},
{"object", {
{"currency", "USD"},
{"value", 42.99}
}}
};
对于数组[]
,可以使用json::array()
生成;对于对象{}
,可以使用json::object()
生成。
序列化/反序列化
变为/来自字符串
之前提到,可以使用命名空间nlohman::literals
中的_json
字面量和json::parse()
来反序列化一个字符串。
要想序列化一个JSON对象为字符串,可以使用json.dump()
:
// explicit conversion to string
std::string s = j.dump(); // {"happy":true,"pi":3.141}
// serialization with pretty printing
// pass in the amount of spaces to indent
std::cout << j.dump(4) << std::endl;
// {
// "happy": true,
// "pi": 3.141
// }
json.dump()
只支持UTF-8编码的字符,如果使用其他编码会抛出异常。
需要注意的是,“在json
中存储一个字符串值”与“将一个字符串值序列化为json
对象”这两个操作并不一样。
变为/来自流
也能用流(如输入输出流,文件流,字符串流)来进行序列化/反序列化操作。下文是使用输入输出流进行序列化/反序列化操作的例子:
// deserialize from standard input
json j;
std::cin >> j;
// serialize to standard output
std::cout << j;
// the setw manipulator was overloaded to set the indentation for pretty printing
std::cout << std::setw(4) << j << std::endl;
还有使用文件流进行序列化/反序列化操作的例子:
// read a JSON file
std::ifstream i("file.json");
json j;
i >> j;
// write prettified JSON to another file
std::ofstream o("pretty.json");
o << std::setw(4) << j << std::endl;
STL风格接口
json
实现了STL容器风格的接口:
// create an array using push_back
json j;
j.push_back("foo");
j.push_back(1);
j.push_back(true);
// also use emplace_back
j.emplace_back(1.78);
// iterate the array
for (json::iterator it = j.begin(); it != j.end(); ++it) {
std::cout << *it << '\n';
}
// range-based for
for (auto& element : j) {
std::cout << element << '\n';
}
// getter/setter
const auto tmp = j[0].template get<std::string>();
j[1] = 42;
bool foo = j.at(2);
// comparison
j == R"(["foo", 1, true, 1.78])"_json; // true
// other stuff
j.size(); // 4 entries
j.empty(); // false
j.type(); // json::value_t::array
j.clear(); // the array is empty again
// convenience type checkers
j.is_null();
j.is_boolean();
j.is_number();
j.is_object();
j.is_array();
j.is_string();
// create an object
json o;
o["foo"] = 23;
o["bar"] = false;
o["baz"] = 3.141;
// also use emplace
o.emplace("weather", "sunny");
// special iterator member functions for objects
for (json::iterator it = o.begin(); it != o.end(); ++it) {
std::cout << it.key() << " : " << it.value() << "\n";
}
// the same code as range for
for (auto& el : o.items()) {
std::cout << el.key() << " : " << el.value() << "\n";
}
// even easier with structured bindings (C++17)
for (auto& [key, value] : o.items()) {
std::cout << key << " : " << value << "\n";
}
// find an entry
if (o.contains("foo")) {
// there is an entry with key "foo"
}
// or via find and an iterator
if (o.find("foo") != o.end()) {
// there is an entry with key "foo"
}
// or simpler using count()
int foo_present = o.count("foo"); // 1
int fob_present = o.count("fob"); // 0
// delete an entry
o.erase("foo");
转换STL容器
json
提供将STL容器转换为json
对象的接口,包括顺序容器和关联容器。
顺序容器
std::vector<int> c_vector {1, 2, 3, 4};
json j_vec(c_vector);
// [1, 2, 3, 4]
std::deque<double> c_deque {1.2, 2.3, 3.4, 5.6};
json j_deque(c_deque);
// [1.2, 2.3, 3.4, 5.6]
std::list<bool> c_list {true, true, false, true};
json j_list(c_list);
// [true, true, false, true]
std::forward_list<int64_t> c_flist {12345678909876, 23456789098765, 34567890987654, 45678909876543};
json j_flist(c_flist);
// [12345678909876, 23456789098765, 34567890987654, 45678909876543]
std::array<unsigned long, 4> c_array {{1, 2, 3, 4}};
json j_array(c_array);
// [1, 2, 3, 4]
关联容器
std::set<std::string> c_set {"one", "two", "three", "four", "one"};
json j_set(c_set); // only one entry for "one" is used
// ["four", "one", "three", "two"]
std::unordered_set<std::string> c_uset {"one", "two", "three", "four", "one"};
json j_uset(c_uset); // only one entry for "one" is used
// maybe ["two", "three", "four", "one"]
std::multiset<std::string> c_mset {"one", "two", "one", "four"};
json j_mset(c_mset); // both entries for "one" are used
// maybe ["one", "two", "one", "four"]
std::unordered_multiset<std::string> c_umset {"one", "two", "one", "four"};
json j_umset(c_umset); // both entries for "one" are used
// maybe ["one", "two", "one", "four"]
std::map<std::string, int> c_map { {"one", 1}, {"two", 2}, {"three", 3} };
json j_map(c_map);
// {"one": 1, "three": 3, "two": 2 }
std::unordered_map<const char*, double> c_umap { {"one", 1.2}, {"two", 2.3}, {"three", 3.4} };
json j_umap(c_umap);
// {"one": 1.2, "two": 2.3, "three": 3.4}
std::multimap<std::string, bool> c_mmap { {"one", true}, {"two", true}, {"three", false}, {"three", true} };
json j_mmap(c_mmap); // only one entry for key "three" is used
// maybe {"one": true, "two": true, "three": true}
std::unordered_multimap<std::string, bool> c_ummap { {"one", true}, {"two", true}, {"three", false}, {"three", true} };
json j_ummap(c_ummap); // only one entry for key "three" is used
// maybe {"one": true, "two": true, "three": true}
Json Pointer和Json Patch
Json Pointer
Json Pointer可以快速在Json对象中定位想要的值:
// a JSON value
json j_original = R"({
"baz": ["one", "two", "three"],
"foo": "bar"
})"_json;
// access members with a JSON pointer (RFC 6901)
j_original["/baz/1"_json_pointer];
// "two"
Json Patch
Json Patch可以通过指令的方式修改Json对象:
// a JSON patch (RFC 6902)
json j_patch = R"([
{ "op": "replace", "path": "/baz", "value": "boo" },
{ "op": "add", "path": "/hello", "value": ["world"] },
{ "op": "remove", "path": "/foo"}
])"_json;
// apply the patch
// 将j_original中的内容按照j_patch的指令修改
json j_result = j_original.patch(j_patch);
// {
// "baz": "boo",
// "hello": ["world"]
// }
// calculate a JSON patch from two JSON values
// 获得从j_result变为j_original需要的Json Patch
json::diff(j_result, j_original);
// [
// { "op":" replace", "path": "/baz", "value": ["one", "two", "three"] },
// { "op": "remove","path": "/hello" },
// { "op": "add", "path": "/foo", "value": "bar" }
// ]
此外,还能通过在原文档基础上覆写的方式修改Json对象:
// a JSON value
json j_document = R"({
"a": "b",
"c": {
"d": "e",
"f": "g"
}
})"_json;
// a patch
json j_patch = R"({
"a":"z",
"c": {
"f": null
}
})"_json;
// apply the patch
j_document.merge_patch(j_patch);
// {
// "a": "z",
// "c": {
// "d": "e"
// }
// }
转换自定义对象
可以通过在对象命名空间中编写from_json()
和to_json
以实现自定义对象的序列化/反序列化操作。
例如有一个对象:
namespace ns {
// a simple struct to model a person
struct person {
std::string name;
std::string address;
int age;
};
}
为其编写相关函数:
using json = nlohmann::json;
namespace ns {
void to_json(json& j, const person& p) {
j = json{{"name", p.name}, {"address", p.address}, {"age", p.age}};
}
void from_json(const json& j, person& p) {
j.at("name").get_to(p.name);
j.at("address").get_to(p.address);
j.at("age").get_to(p.age);
}
} // namespace ns
这样,当json
对ns::person
进行构造时,将调用to_json()
;调用get<type>()
或get_to(type&)
时,将调用from_json()
。
然后就能这样子使用了:
// create a person
ns::person p {"Ned Flanders", "744 Evergreen Terrace", 60};
// conversion: person -> json
json j = p;
std::cout << j << std::endl;
// {"address":"744 Evergreen Terrace","age":60,"name":"Ned Flanders"}
// conversion: json -> person
auto p2 = j.template get<ns::person>();
// that's it
assert(p == p2);
转换第三方库对象
对于第三方库对象,需要在命名空间nlohmann
中实现adl_serializer
的特化:
// 例: 序列化boost库对象
// partial specialization (full specialization works too)
namespace nlohmann {
template <typename T>
struct adl_serializer<boost::optional<T>> {
static void to_json(json& j, const boost::optional<T>& opt) {
if (opt == boost::none) {
j = nullptr;
} else {
j = *opt; // this will call adl_serializer<T>::to_json which will
// find the free function to_json in T's namespace!
}
}
static void from_json(const json& j, boost::optional<T>& opt) {
if (j.is_null()) {
opt = boost::none;
} else {
opt = j.template get<T>(); // same as above, but with
// adl_serializer<T>::from_json
}
}
};
}
使用宏简化操作
如果有如下需求:
- 用JSON进行序列化操作;
- 用成员变量名作为JSON对象中的
key
;
就能使用宏简化对结构体的操作,而不是为每一个结构体编写to_json()
和from_json()
。
枚举
对于枚举的序列化/反序列化,可以使用NLOHMANN_JSON_SERIALIZE_ENUM。json
默认会将枚举序列化为整数值导致二义性,该宏可以将为枚举值映射为字符串:
// example enum type declaration
enum TaskState {
TS_STOPPED,
TS_RUNNING,
TS_COMPLETED,
TS_INVALID=-1,
};
// map TaskState values to JSON as strings
NLOHMANN_JSON_SERIALIZE_ENUM( TaskState, {
{TS_INVALID, nullptr},
{TS_STOPPED, "stopped"},
{TS_RUNNING, "running"},
{TS_COMPLETED, "completed"},
})
类和结构体
对于类和结构体的序列化/反序列化,可以使用如下的宏:
- NLOHMANN_DEFINE_TYPE_INTRUSIVE - 序列化/反序列化一个带有私有成员的非派生类;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT - 在上一个的基础上带有默认值;
- NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE - 只序列化一个带有私有成员的非派生类;
- NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE - 序列化/反序列化一个非派生类;
- NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT - 在上一个的基础上带有默认值;
- NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE - 只序列化一个非派生类;
对于之前的person
对象,他没有私有成员,而且是非派生类,使用NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE
即可:
namespace ns {
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(person, name, address, age)
}
接下来看看带有私有成员的NLOHMANN_DEFINE_TYPE_INTRUSIVE
例子:
namespace ns {
class address {
private:
std::string street;
int housenumber;
int postcode;
public:
NLOHMANN_DEFINE_TYPE_INTRUSIVE(address, street, housenumber, postcode)
};
}
二进制压缩
为了节省存储空间和方便数据交互,json
支持多种二进制格式,详见这里。
实践
接下来开始利用nlohmann/json
库封装自己的序列化/反序列化库,详见我的毕业设计中Engine/Source/Runtime/Core/Serialize
部分。
参考资料
- Inja: Main Page (pantor.github.io)