用C++实现一个轻量级Json库
source link: https://xuranus.github.io/2022/11/27/%E7%94%A8C-%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%BD%BB%E9%87%8F%E7%BA%A7Json%E5%BA%93/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
用C++实现一个轻量级Json库
2022-11-272023-03-110 Comments 31k 28 分钟
JSON是非常常用的结构对象序列化方式。相比于其他基于字节流的序列化方案(thrift,protobuff),JSON易于阅读和编辑;相较于同样基于文本的序列化方案XML,JSON在网络传输占用更小的空间。JSON很好平衡了序列化/反序列化的性能、可读性、空间占用的问题。
很多语言都在标准库中添加了JSON相关的API,但C/C++还没有。C++较为完善的开源JSON库有jsoncpp。这篇文件将介绍如何用1K行左右的代码实现一个精简C++的Json库。
在开始之前强烈建议读一下JSON官方描述文档,其中包含了JSON格式的详细定义,之后实现JSON解析的时候将会用得到。
一个JSON成员(JsonElement)可以是一个null
、数字(number)、布尔值(bool)、字符串(string)这些基础类型;也可以是一个JSON数组(JsonArray)、JSON对象(JsonObject)这类复合类型。所以这6类类型可以看作是一个JsonElement
:
JsonElement
应该提供以下能力:
- 记录自己对应的类型信息,并能转化成对应六种的类型之一
- 提供统一序列化接口,对每一种类型分别实现序列化
于是首先定义一个序列化接口,所有的JsonElement
都应该实现这个接口。
class Serializable {
virtual std::string Serialize() const = 0;
};
可以定义一个JsonElement
类,用枚举类型标记其是哪一种类型,并用联合体成员记存放其对应实际的值:
class JsonElement: public Serializable {
public:
enum class Type {
JSON_OBJECT,
JSON_ARRAY,
JSON_STRING,
JSON_NUMBER,
JSON_BOOL,
JSON_NULL
};
union Value {
JsonObject* objectValue;
JsonArray* arrayValue;
std::string* stringValue;
double numberValue;
bool boolValue;
};
std::string Serialize() const override;
private:
Type m_type = Type::JSON_NULL;
Value m_value {};
};
其中基础类型number类型用double
表示,用bool
表示布尔值,std::string
表示字符串。null类型不需要存放值,只需要在m_type
中标记类型即可。对于复合类型JSON数组和JSON对象用对应的指针类型来存放。
由于JsonElement
需要提供对于各种类型的转换能力,需要定义从各种基础类型的构造方法,并提供转换成对应类型的AsXXX()
和ToXXX()
方法。在操作JsonElement
对象时需要用TypeName()
和IsXXX()
判断JsonElement
存放的的具体类型。API设计如下:
class JsonElement: public Serializable {
public:
JsonElement();
explicit JsonElement(JsonElement::Type type);
explicit JsonElement(bool value);
explicit JsonElement(double num);
explicit JsonElement(long num);
explicit JsonElement(const std::string &str);
explicit JsonElement(char const *str);
JsonElement(const JsonObject& object);
JsonElement(const JsonArray& array);
JsonElement(const JsonElement& ele);
explicit JsonElement(JsonElement&& ele);
JsonElement& operator = (const JsonElement& ele);
~JsonElement();
bool& AsBool();
double& AsNumber();
void* AsNull() const;
std::string& AsString();
JsonObject& AsJsonObject();
JsonArray& AsJsonArray();
bool ToBool() const;
double ToNumber() const;
void* ToNull() const;
std::string ToString() const;
JsonObject ToJsonObject() const;
JsonArray ToJsonArray() const;
bool IsNull() const;
bool IsBool() const;
bool IsNumber() const;
bool IsString() const;
bool IsJsonObject() const;
bool IsJsonArray() const;
std::string TypeName() const;
std::string Serialize() const override;
};
复合类型的JSON对象(JsonObject)和JSON数组(JsonArray)可以包含JsonElement。
JsonObject可以看作KV结构的字典,Key是字符串,Value是JsonElement类型;JsonArray可以看成JsonElement构成的数组。于是这里分别选用继承std::vector<JsonElement>
和std::map<std::string, JsonElement>
来实现JsonArray和JsonObject:
class JsonObject: public std::map<std::string, JsonElement>, public Serializable {
public:
std::string Serialize() const override;
};
class JsonArray: public std::vector<JsonElement>, public Serializable {
public:
std::string Serialize() const override;
};
具体API的实现代码有400多行,都是比较简单的逻辑判断。这里就不详述了,详细见:Json.cpp
有了基本的API设计和实现,就已经可以开始实现序列化了。序列化较为简单,只需要实现JsonElement::Serialize()
、JsonArray::Serialize()
和JsonObject::Serialize()
方法。
JsonElement::Serialize()
中实现五种基础类型的序列化,null
和布尔值的true
和false
直接返回对应的字面量即可,数字类型可以用std::to_string
直接获得字面量,然后删除末尾冗余的浮点0(详见Json.cpp中DoubleToString()
。字符串序列化需要前后带上引号,需要注意特殊字符的转义。JsonObject
和JsonArray
两种复合类型在对应自身的Serialize()
中分别实现:
std::string JsonElement::Serialize() const
{
switch (m_type) {
case JsonElement::Type::JSON_NULL: {
return "null";
}
case JsonElement::Type::JSON_BOOL: {
return m_value.boolValue ? "true" : "false";
}
case JsonElement::Type::JSON_NUMBER: {
return DoubleToString(m_value.numberValue);
}
case JsonElement::Type::JSON_STRING: {
return std::string("\"") + EscapeString(*m_value.stringValue) + "\"";
}
case JsonElement::Type::JSON_OBJECT: {
return JsonObject(*m_value.objectValue).Serialize();
}
case JsonElement::Type::JSON_ARRAY: {
return JsonArray(*m_value.arrayValue).Serialize();
}
}
Panic("unknown type to serialize: %s", TypeName().c_str());
return "";
}
字符串转义需要考虑"
,\
,/
,\f
,\b
,\n
,\r
,\t
这类字符,此处提供的实现如下:
std::string util::EscapeString(const std::string& str)
{
std::string res;
for (const char ch: str) {
switch (ch) {
case '"':
case '\\':
case '/':
res.push_back('\\');
res.push_back(ch);
break;
case '\f': {
res.push_back('\\');
res.push_back('f');
break;
}
case '\b': {
res.push_back('\\');
res.push_back('b');
break;
}
case '\r': {
res.push_back('\\');
res.push_back('r');
break;
}
case '\n': {
res.push_back('\\');
res.push_back('n');
break;
}
case '\t': {
res.push_back('\\');
res.push_back('t');
break;
}
default: {
res.push_back(ch);
break;
}
}
}
return res;
}
JsonObject
序列化是一个遍历所有的KV对,递归调用字符串类型KEY和JsonElement
类型的JsonElement::Serialize
的过程:
std::string JsonObject::Serialize() const
{
std::string res = "{";
bool moreThanOneItem = false;
for (const auto& kv: *this) {
moreThanOneItem = true;
res += std::string("\"") + EscapeString(kv.first) + "\":" + kv.second.Serialize() + ",";
}
if (moreThanOneItem) {
res.pop_back();
}
res += "}";
return res;
}
JsonArray
的序列化则是迭代调用所有数组成员类型JsonElement::Serialize
的过程:
std::string JsonArray::Serialize() const
{
std::string res = "[";
bool moreThanOne = false;
for (const auto& v: *this) {
moreThanOne = true;
res += v.Serialize() + ",";
}
if (moreThanOne) {
res.pop_back();
}
res += "]";
return res;
}
写个Demo验证序列化成果:
TEST(SerializationTest, JsonBasicSerializationTest) {
JsonObject object {};
object["name"] = JsonElement("xuranus");
object["age"] = JsonElement(300.0);
object["isDeveloper"] = JsonElement(true);
object["skills"] = JsonElement(null);
EXPECT_EQ(object.Serialize(), R"({"age":300,"name":"xuranus", "isDeveloper":true, "skills":null})");
}
TOKEN
反序列化需要实现一个JSON字符串扫描和解析的模块,传入一个JSON字符串转为一个JsonElement
。为了降低实现的复杂度,这里将解析的模块分成两块:
JsonScanner
:逐字节扫描输入的字符串,依次识别一个个有意义的JSON语法TOKEN,并逐个返回JsonParser
:基于JsonScanner
返回的TOKEN流,依据JSON的格式定义解析完整的JSON成员
JSON的TOKEN是一个个较为简单的语法单位,这里定义如下的TOKEN:
WHITESPACE
: 空白符 (' '
,'\n'
,'\r'
,'\t'
)NUMBER
: 数字,例如3.14
、114514
、1919.810
STRING
: 字符串,例如"xuranus"
LITERAL_TRUE
:true
字面量LITERAL_FALSE
:false
字面量LITERAL_NULL
:null
字面量COMMA
: 逗号,
COLON
: 冒号:
ARRAY_BEGIN
: JSON数组开始符[
ARRAY_END
: JSON数组结束符]
OBJECT_BEGIN
: JSON对象开始符{
OBJECT_END
: JSON对象结束符}
EOF_TOKEN
: 标记串末尾EOF
举个例子:
{
"name" : "xuranus",
"age" : 114514,
"skills": ["Java", "CPP", "JS"]
}
如果忽略所有的空白符,该JSON字符产长这样:
{"name":"xuranus","ID":114514,"skills":["Java","CPP","JS"]}
按照上述Token定义,我们对该字符串进行Token解析,可以获得Token流:
Token | Token | 字符串 | 数字 |
---|---|---|---|
{ |
OBJECT_BEGIN | ||
"name" |
STRING | name | |
: |
COLON | ||
"xuranus" |
STRING | xuranus | |
, |
COMMA | ||
"ID" |
STRING | ID | |
: |
COLON | ||
114514 |
NUMBER | 114514 | |
, |
COMMA | ||
"skills" |
STRING | skills | |
: |
COLON | ||
[ |
ARRAY_BEGIN | ||
"Java" |
STRING | Java | |
, |
COMMA | ||
"CPP" |
STRING | CPP | |
, |
COMMA | ||
"JS" |
STRING | JS | |
] |
ARRAY_END | ||
} |
OBJECT_END |
从Token流可以进一步解析出JSON的AST:
明确了原理,接下来就来分别看看JsonScanner
和JsonParser
的实现。
JsonScanner
给JsonScanner
将在内部存放一个带解析的JSON字符串std::string m_str
并维护一个指向其当前读取位置的的下标索引std::size_t m_pos
。JsonScanner
将提供一个返回Token的Next()
方法供外部调用者调用,返回下一个解析出的Token。
在上述的13种Token种,数字Token(NUMBER
)和字符串Token(STRING
)解析起来比较复杂,我们将其解析逻辑放入单独的两个解析方法:
double GetNumberValue();
std::string GetStringValue();
他们解析的类型依然由Next()
方法返回,但是他们解析出的值需要单独存档在类成员中,可以用std::string m_tmpStrValue
和double m_tmpNumberValue
来暂存解析出的值,并由以上两个方法返回。其余的11种Token解析基本拥有固定的字面量(例如LITERAL_TRUE
就是true
、ARRAY_BEGIN
就是[
,拿到了Token就是拿到了字面量),他们解析起来也较为容易,解析逻辑将在Next()
中实现。
基于以上思路,就可以给出JsonScanner
的定义:
class JsonScanner {
public:
enum class Token {
WHITESPACE, // ' ', '\n', '\r', '\t'
NUMBER,
STRING,
LITERAL_TRUE, // true
LITERAL_FALSE, // false
LITERAL_NULL, // null
COMMA, // ,
COLON, // :
ARRAY_BEGIN, // [
ARRAY_END, // ]
OBJECT_BEGIN, // {
OBJECT_END, // }
EOF_TOKEN // mark the end of the json string
};
public:
JsonScanner(const std::string &str);
void Reset();
Token Next();
double GetNumberValue();
std::string GetStringValue();
inline void RollBack() { m_pos = m_prevPos; }
inline size_t Position() { return m_pos; }
private:
void ScanNextString();
void ScanNextNumber();
private:
std::string m_str;
std::size_t m_pos = 0;
std::size_t m_prevPos = 0;
std::string m_tmpStrValue {};
double m_tmpNumberValue {0};
};
其中Reset()
、RollBack()
、Position()
用于设置当前索引下标的跳转以辅助解析流程,之后再看他们的实现。
void JsonScanner::Reset() { m_pos = 0; m_prevPos = 0; }
首先看Next()
方法,这是Token解析最核心的部分。Next()
的任务是返回下一个Token,在解析下一个Token之前一般需要先跳过空白符,空白符(' '
、'\n'
、'\r'
、'\t'
)的存在一般只是用于缩进为了方便阅读JSON,而对JSON的解析毫无意义。空白符可以存在任何两个Token之间,所以要先跳过空白符。这里提供跳过空白符的实现:
class JsonScanner {
//...
inline bool IsWhiltespaceToken(char ch)
{
return (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t');
}
inline bool SkipWhitespaceToken()
{
while (m_pos < m_str.size() && IsWhiltespaceToken(m_str[m_pos])) {
m_pos++;
}
return m_pos < m_str.size();
}
};
其中SkipWhitespaceToken
将会在解析到字符串末尾时返回false
。在每次指向Next()
都应该检测解析是否结束,如果结束则返回EOF_TOKEN
。当拿到下一个非空白符的字符时,就可以开始解析Token了:如果第一个字符是-
或者数字,则下一个Token有可能是一个数字,此时则调用ScanNextNumber
进入数字解析模式;如果当前字符是"
,则下一个Token可能是一个字符串;如果当前字符串是t
、f
、n
,则下一个Token可能是LITERAL_TRUE
、LITERAL_FALSE
、LITERAL_NULL
,他们的Token对应的字面量固定,用ScanLiteral()
检测。ScanLiteral()
用于检测之后的字符串是否匹配给定的字面量,失败则用Panic
抛异常,实现如下:
inline void Panic(const char* str, ...)
{
char message[100];
va_list args;
va_start(args, str);
vsprintf(message, str, args);
va_end(args);
throw std::logic_error(message);
}
class JsonScanner {
// ...
inline void ScanLiteral(const std::string& literal, int offset)
{
if (m_str.compare(m_pos, offset, literal) == 0) {
m_pos += offset;
} else {
Panic("unknown literal token at position = %lu, do you mean: %s ?", m_pos, literal.c_str());
}
}
};
最后,ARRAY_BEGIN
、OBJECT_BEGIN
等剩余的Token长度为1,直接用当前字符就能判断并返回。终于,基于以上逻辑,给出Next()
的实现:
// return a non space token
JsonScanner::Token JsonScanner::Next()
{
m_prevPos = m_pos;
if (m_str.length() <= m_pos || !SkipWhitespaceToken()) {
return Token::EOF_TOKEN;
}
char curChar = m_str[m_pos];
if (IsDigit(curChar) || curChar == '-') {
ScanNextNumber();
return Token::NUMBER;
}
switch (curChar) {
case '\"':
ScanNextString();
return Token::STRING;
case 't':
ScanLiteral("true", 4);
return Token::LITERAL_TRUE;
case 'f':
ScanLiteral("false", 5);
return Token::LITERAL_FALSE;
case 'n':
ScanLiteral("null", 4);
return Token::LITERAL_NULL;
case '[':
m_pos ++;
return Token::ARRAY_BEGIN;
case ']':
m_pos ++;
return Token::ARRAY_END;
case '{':
m_pos ++;
return Token::OBJECT_BEGIN;
case '}':
m_pos ++;
return Token::OBJECT_END;
case ',':
m_pos ++;
return Token::COMMA;
case ':':
m_pos ++;
return Token::COLON;
}
Panic("Invalid token at position %lu", m_pos);
return Token::LITERAL_NULL;
}
接着来看Next()
中用于识别下一个字符串的ScanNextString()
方法。ScanNextString()
从当前位置开始扫描字符串,如果成果就将结果存在m_tmpStrValue
中。字符串以"
开始、以"
结束,由于在调用该方法之前Next()
方法总已经扫到了字符串开始的"
,接下来扫描字符串也就是要找到下一个"
,取其中的值作为字符串值。
在遍历的过程中需要注意特殊字符的转义。由于JSON序列化后的字符串中特殊字符已经被转义成了占用2字节的字面量,例如'\n'
、'\r'
,或者也可能包含占用5个字符unicode编码字面量,例如\u1289
。在反序列化的过程中需要检测\
字符,将其视为转义字符。当遇到转义符时,将额外读取1个或4个字符。在解析到下一个"
时,字符串读取完毕,此时该字符字串是含有转义字符字面量的串,需要再次进行反转义获得原始字符串。
ScanNextString()
和字符串反转义实现如下:
void JsonScanner::ScanNextString()
{
size_t beginPos = m_pos;
m_pos ++; // skip left "
while (m_pos < m_str.size() && m_str[m_pos] != '\"') {
char curChar = m_str[m_pos ++];
if (curChar == '\\') {
// " quotation mark
// \ reverse soildus
// / sodilus
// b backspace
// f formfeed
// n linefeed
// r carriage return
// t horizontal tab
// u (4 hex digits)
if (m_pos >= m_str.size()) {
Panic("missing token, position: %lu", m_pos);
return;
} else {
char escapeChar = m_str[m_pos];
if (escapeChar == '\"' || escapeChar == 'r' || escapeChar == 'f' || escapeChar == 'n' ||
escapeChar == 't' || escapeChar == 'b' || escapeChar == '\\' || escapeChar == '/') {
m_pos ++;
} else if (escapeChar == 'u') {
m_pos += 4;
}
}
}
}
if (m_pos >= m_str.size()) {
Panic("missing end of string, position: %lu", beginPos);
}
m_pos ++; // skip right "
std::string rawStr = m_str.substr(beginPos + 1, m_pos - beginPos - 2);
m_tmpStrValue = util::UnescapeString(rawStr);
}
std::string util::UnescapeString(const std::string& str)
{
std::string res;
for (size_t i = 0; i < str.size(); ++i) {
char curChar = str[i];
if (curChar == '\\' && i + 1 < str.size()) {
char escapeChar = str[++i];
switch (escapeChar) {
case '"':
res.push_back('"');
break;
case '\\':
res.push_back('\\');
break;
case '/':
res.push_back('/');
break;
case 'f': {
res.push_back('\f');
break;
}
case 'b': {
res.push_back('\b');
break;
}
case 'r': {
res.push_back('\r');
break;
}
case 'n': {
res.push_back('\n');
break;
}
case 't': {
res.push_back('\t');
break;
}
Panic("invalid escaped char \\%c", escapeChar);
}
} else {
res.push_back(curChar);
}
}
return res;
}
std::string JsonScanner::GetStringValue() { return m_tmpStrValue; }
除了字符串解析,另一个复杂Token的解析就是数字了。JSON中的数字可能是整数,也可能是浮点数;可能带符号,也可能不带符号,浮点数还可能是科学计数法表示。数字解析相关实现如下:
class JsonScanner {
// ...
inline bool IsDigit(char ch)
{
return '0' <= ch && ch <= '9';
}
};
void JsonScanner::ScanNextNumber()
{
size_t beginPos = m_pos;
// example: "-114.51E-4"
m_pos ++; // skip + or - or first digit
while (m_pos < m_str.size() && IsDigit(m_str[m_pos])) {
m_pos ++;
}
if (m_pos + 1 < m_str.size() && m_str[m_pos] == '.' && IsDigit(m_str[m_pos + 1])) {
m_pos ++; // skip .
while(m_pos < m_str.size() && IsDigit(m_str[m_pos])) {
m_pos ++;
}
}
if (m_pos + 1 < m_str.size() && (m_str[m_pos] == 'E' || m_str[m_pos] == 'e')) {
m_pos ++;
if (m_str[m_pos] == '-' || m_str[m_pos] == '+') {
m_pos ++;
}
// parse number
while (m_pos < m_str.size() && IsDigit(m_str[m_pos])) {
m_pos ++;
}
}
std::string numberStr = m_str.substr(beginPos, m_pos - beginPos);
try {
m_tmpNumberValue = std::atof(numberStr.c_str());
} catch (std::exception &e) {
Panic("invalid number %lf, pos: %lu", m_tmpNumberValue, beginPos);
}
}
double JsonScanner::GetNumberValue() { return m_tmpNumberValue; }
此时,JsonScanner
的全部代码就实现完了,我们已经获得了一个对JSON字符串实现词法分析的引擎。接下来我们只需要在此基础上基于Token流解析完整的JSON结构。
JsonParser
由于对JSON的解析依赖于JsonScanner
提供的Token解析,JsonParser
内部持有一个JsonScanner
的instance。
仿照之前JsonScanner
的设计方式,将解析流程分成简单结构的解析方法和复杂结构的解析方法,JsonParser
可以将数字、布尔值、字符串等基本结构放在一个ParseNext()
方法中,而复杂点的复合结构JsonArray
和JsonObject
则由独立的ParseJsonArray()
和ParseJsonObject
负责解析。
给出JsonParser
定义如下:
class JsonParser {
private:
JsonScanner m_scanner;
public:
JsonParser(const std::string str);
JsonElement Parse();
private:
JsonElement ParseNext();
JsonObject ParseJsonObject();
JsonArray ParseJsonArray();
};
正如JsonScanner
从前往后解析TOKEN,JsonParser
从前往后解析JsonElement
。JsonParser::ParseNext
是JsonParser
的核心逻辑,他返回下一个解析出的JsonElement
。类似JsonScanner
根据当前字符推断下一个可能的Token,JsonParser
根据当前Token推断下一个可能的结构,所以ParseNext()
的实现十分简单:
JsonElement JsonParser::ParseNext()
{
JsonScanner::Token token = m_scanner.Next();
switch (token) {
case JsonScanner::Token::OBJECT_BEGIN: {
return ParseJsonObject();
}
case JsonScanner::Token::ARRAY_BEGIN: {
return ParseJsonArray();
}
case JsonScanner::Token::STRING: {
return JsonElement(m_scanner.GetStringValue());
}
case JsonScanner::Token::NUMBER: {
return JsonElement(m_scanner.GetNumberValue());
}
case JsonScanner::Token::LITERAL_TRUE: {
return JsonElement(true);
}
case JsonScanner::Token::LITERAL_FALSE: {
return JsonElement(false);
}
case JsonScanner::Token::LITERAL_NULL: {
return JsonElement();
}
}
Panic("scanner return unexpected token: %d", token);
return JsonElement();
}
其中字符串、布尔、数字、null这四种基础类型可以直接用JsonScanner
提供的TOKEN解析能力直接拿到。而JsonArray
和JsonObject
的解析较为复杂,则放到单独的ParseJsonObject()
和ParseJsonArray()
方法中实现。
ParseJsonObject()
本质上就是解析STRING
和COLON
,然后再递归调用ParseNext()
解析value对象,直到遇到OBJECT_END
退出:
JsonObject JsonParser::ParseJsonObject()
{
JsonObject object {};
JsonScanner::Token token = m_scanner.Next();
if (token == JsonScanner::Token::OBJECT_END) {
return object;
}
m_scanner.RollBack();
while (true) {
size_t pos = m_scanner.Position();
token = m_scanner.Next();
if (token != JsonScanner::Token::STRING) {
Panic("expect a string as key for json object, position: %lu", pos);
}
std::string key = m_scanner.GetStringValue();
pos = m_scanner.Position();
token = m_scanner.Next();
if (token != JsonScanner::Token::COLON) {
Panic("expect ':' in json object, position: %lu", pos);
}
JsonElement ele = ParseNext();
object[key] = ele;
pos = m_scanner.Position();
token = m_scanner.Next();
if (token == JsonScanner::Token::OBJECT_END) {
break;
}
if (token != JsonScanner::Token::COMMA) {
Panic("expect ',' in json object, position: %lu", pos);
}
}
return object;
}
解析数组的原理同理,先解析ARRAY_BEGIN
,然后依次递归调用ParseNext()
解析下一个JsonElement
对象,知道遇到ARRAY_END
完成解析。
JsonArray JsonParser::ParseJsonArray()
{
JsonArray array {};
JsonScanner::Token token = m_scanner.Next();
if (token == JsonScanner::Token::ARRAY_END) {
return array;
}
m_scanner.RollBack();
while (true) {
array.push_back(ParseNext());
size_t pos = m_scanner.Position();
token = m_scanner.Next();
if (token == JsonScanner::Token::ARRAY_END) {
break;
}
if (token != JsonScanner::Token::COMMA) {
Panic("expect ',' in array, pos: %lu", pos);
}
}
return array;
}
由于进入
ParseJsonArray()
和ParseJsonObject()
之前JsonScanner
的字符串索引已经指向了[
或{
,进入对应方法后需要RollBack()
回滚索引。
到此位置,反序列化也完全实现了。接下来写个用例验证下:
TEST(SerializationTest, JsonParserBasicTest) {
std::string str = R"(
{
"name" : "xuranus",
"age" : 300,
"skills" : ["C++", "Java", "Python"]
}
)";
JsonElement element = JsonParser(str).Parse();
JsonObject object = element.AsJsonObject();
EXPECT_EQ(object["name"].AsString(), "xuranus");
EXPECT_EQ(object["age"].AsNumber(), 300);
EXPECT_EQ(object["skills"].AsJsonArray()[0].AsString(), "C++");
EXPECT_EQ(object["skills"].AsJsonArray()[1].AsString(), "Java");
EXPECT_EQ(object["skills"].AsJsonArray()[2].AsString(), "Python");
}
此时,虽然一个完整的JSON序列化和反序列化工具已经实现,但是用起来还是比较麻烦。在Java、GO中都有注解实现的ORM框架,能实现类/结构体的自动序列化/反序列化,我们尝试在C++中也实现这一功能。
在下面的样例中,我们期望实现两个函数:
std::string util::Serialize(T&)
将类型为T的结构体序列化成JSON字符串void util::Deserialize(const std::string&, T&)
将JSON字符串反序列化成对应的结构体为了实现对每一种类型的结构体都能序列化,我们必须对其中成员序列化映射规则做出定义。为了实现这点,我们不得不在结构体中侵入式的定义一个成员函数struct Book {
std::string m_name;
int m_id;
float m_price;
bool m_soldOut;
std::vector<std::string> m_tags;
};
TEST(SerializationTest, BasicStructSerialization) {
Book book1 {};
book1.m_name = "C++ Primer";
book1.m_id = 114514;
book1.m_price = 114.5;
book1.m_soldOut = true;
book1.m_tags = {"C++", "Programming", "Language"};
// serialization here
std::string jsonStr = util::Serialize(book1);
Book book2 {};
// deserialization here
util::Deserialize(jsonStr, book2);
EXPECT_EQ(book1.m_name, book2.m_name);
EXPECT_EQ(book1.m_id, book2.m_id);
EXPECT_EQ(book1.m_price, book2.m_price);
EXPECT_EQ(book1.m_soldOut, book2.m_soldOut);
EXPECT_EQ(book1.m_tags, book2.m_tags);
}_XURANUS_JSON_CPP_SERIALIZE_METHOD_
:我们可以用宏定义简化这部分的表示:struct Book {
std::string m_name;
int m_id;
float m_price;
bool m_soldOut;
std::vector<std::string> m_tags;
void _XURANUS_JSON_CPP_SERIALIZE_METHOD_(xuranus::minijson::JsonObject& object, bool toJson) {
if (toJson) {
util::SerializeTo(object, "name", m_name);
util::SerializeTo(object, "id", m_id);
util::SerializeTo(object, "price", m_price);
util::SerializeTo(object, "soldOut", m_soldOut);
util::SerializeTo(object, "tags", m_tags);
} else {
util::DeserializeFrom(object, "name", m_name);
util::DeserializeFrom(object, "id", m_id);
util::DeserializeFrom(object, "price", m_price);
util::DeserializeFrom(object, "soldOut", m_soldOut);
util::DeserializeFrom(object, "tags", m_tags);
}
}
};定义宏之后的结构体声明就可以写成如下形式,由一组#define SERIALIZE_SECTION_BEGIN \
public: \
using __XURANUS_JSON_SERIALIZATION_MAGIC__ = void; \
public: \
void _XURANUS_JSON_CPP_SERIALIZE_METHOD_(xuranus::minijson::JsonObject& object, bool toJson) \
{ \
#define SERIALIZE_SECTION_END \
}; \
#define SERIALIZE_FIELD(KEY_NAME, ATTR_NAME) \
do { \
if (toJson) { \
util::SerializeTo(object, #KEY_NAME, ATTR_NAME); \
} else { \
xuranus::minijson::util::DeserializeFrom(object, #KEY_NAME, ATTR_NAME); \
} \
} while (0) \SERIALIZE_SECTION_BEGIN
和SERIALIZE_SECTION_END
以及一系列SERIALIZE_FIELD
声明成员的序列化/反序列化名称映射关系。:当类定义了上述的成员方法后,就拥有了序列化和反序列化的能力。此时就可以实现供外部调用的接口了:struct Book
{
std::string m_name;
int m_id;
float m_price;
bool m_soldOut;
std::vector<std::string> m_tags;
SERIALIZE_SECTION_BEGIN
SERIALIZE_FIELD(name, m_name);
SERIALIZE_FIELD(id, m_id);
SERIALIZE_FIELD(price, m_price);
SERIALIZE_FIELD(soldOut, m_soldOut);
SERIALIZE_FIELD(tags, m_tags);
SERIALIZE_SECTION_END
};template<typename T>
auto Serialize(T& value) -> decltype(typename T::__XURANUS_JSON_SERIALIZATION_MAGIC__(), std::string())
{
JsonObject object{};
value._XURANUS_JSON_CPP_SERIALIZE_METHOD_(object, true);
return object.Serialize();
}
template<typename T>
auto Deserialize(const std::string& jsonStr, T& value) -> decltype(typename T::__XURANUS_JSON_SERIALIZATION_MAGIC__())
{
JsonParser parser(jsonStr);
JsonElement ele = parser.Parse();
JsonObject object = ele.AsJsonObject();
value._XURANUS_JSON_CPP_SERIALIZE_METHOD_(object, false);
}这里用到了Trailing Return Type,
__XURANUS_JSON_SERIALIZATION_MAGIC__
在之前的成员宏中已经定义为void
,本身并无意义,只是用于标记该类实现了成员序列化方法,用于给序列化/反序列化工具函数在编译期进行检测。如果结构体没定义相关的宏,则模板函数特化时会失败。
于是接下来的任务就只是实现上述_XURANUS_JSON_CPP_SERIALIZE_METHOD_
方法中用到的两个辅助函数:
std::string util::SerializeTo(JsonObject&, const std::string&, T&)
void util::DeserializeFrom(JsonObject&, const std::string&, T&)
首先来看std::string util::SerializeTo(JsonObject&, const std::string&, T&)
。它的用途是把参数3的值以参数2作为key转化成JsonElement
存入参数1的JsonObject
。而void util::DeserializeFrom(JsonObject&, const std::string&, T&)
则相反,它的作用是从参数1的JsonObject
中取出参数2的key对应的JsonElement
,转化为T类型的值存入参数3。这两个模板函数可以实现为:
template<typename T>
void SerializeTo(JsonObject& object, const std::string& key, const T& field)
{
JsonElement ele{};
CastToJsonElement<T>(ele, field);
object[key] = ele;
}
template<typename T>
void DeserializeFrom(const JsonObject& object, const std::string& key, T& field)
{
JsonElement ele = object.find(key)->second;
CastFromJsonElement<T>(ele, field);
}
其中CastToJsonElement(JsonElement & ele, const T & value)
和CastFromJsonElement(const JsonElement & ele, T & value)
是定义了某一JsonElement
和T
类型直线转化关系的模板函数。后续我们需要针对不同的类型逐一实现他们。
如果T
类型本身是一个已经定义了__XURANUS_JSON_SERIALIZATION_MAGIC__
的结构,我们可以递归调用其中的_XURANUS_JSON_CPP_SERIALIZE_METHOD_
方法来实现类型映射:
// cast between struct and JsonElement
template<typename T>
auto CastFromJsonElement(const JsonElement& ele, T& value) -> decltype(typename T::__XURANUS_JSON_SERIALIZATION_MAGIC__()) {
JsonObject object = ele.ToJsonObject();
value._XURANUS_JSON_CPP_SERIALIZE_METHOD_(object, false);
return;
};
template<typename T>
auto CastToJsonElement(JsonElement& ele, const T& value) -> decltype(typename T::__XURANUS_JSON_SERIALIZATION_MAGIC__()) {
JsonObject object{};
T* valueRef = reinterpret_cast<T*>((void*)&value);
valueRef->_XURANUS_JSON_CPP_SERIALIZE_METHOD_(object, true);
ele = JsonElement(object);
return;
};
我们将这复合一类型的模板作为主模板,利用SFINAE来进一步定义更多基础类型的映射模板:
字符串类型:
// cast between std::string and JsonElement
template<typename T, typename std::enable_if<std::is_same<T, std::string>::value>::type* = nullptr>
void CastFromJsonElement(const JsonElement& ele, T& value) {
value = ele.ToString();
return;
}
template<typename T, typename std::enable_if<std::is_same<T, std::string>::value>::type* = nullptr>
void CastToJsonElement(JsonElement& ele, const T& value) {
ele = JsonElement(value);
return;
}
数字类型:
// cast between numeric and JsonElement
template<typename T, typename std::enable_if<(std::is_integral<T>::value || std::is_floating_point<T>::value)
&& !std::is_same<T, bool>::value>::type* = nullptr>
void CastFromJsonElement(const JsonElement& ele, T& value) {
value = static_cast<T>(ele.ToNumber());
return;
}
template<typename T, typename std::enable_if<std::is_integral<T>::value && !std::is_same<T, bool>::value>::type* = nullptr>
void CastToJsonElement(JsonElement& ele, const T& value) {
ele = JsonElement(static_cast<long>(value));
return;
}
template<typename T, typename std::enable_if<std::is_floating_point<T>::value>::type* = nullptr>
void CastToJsonElement(JsonElement& ele, const T& value) {
ele = JsonElement(static_cast<double>(value));
return;
}
布尔类型:
// cast between bool and JsonElement
template<typename T, typename std::enable_if<std::is_same<T, bool>::value>::type* = nullptr>
void CastFromJsonElement(const JsonElement& ele, T& value) {
value = ele.ToBool();
return;
}
template<typename T, typename std::enable_if<std::is_same<T, bool>::value>::type* = nullptr>
void CastToJsonElement(JsonElement& ele, const T& value) {
ele = JsonElement(value);
return;
}
std::vector
和std::list
:
// cast between std::vector or std::list and JsonElement
template<typename T, typename std::enable_if<
std::is_same<T, std::vector<typename T::value_type>>::value ||
std::is_same<T, std::list<typename T::value_type>>::value
>::type* = nullptr>
void CastFromJsonElement(const JsonElement & ele, T & value) {
JsonArray array = ele.ToJsonArray();
value.clear();
for (const JsonElement& eleItem : array) {
typename T::value_type t;
CastFromJsonElement<typename T::value_type>(eleItem, t);
value.push_back(t);
}
return;
}
template<typename T, typename std::enable_if<
std::is_same<T, std::vector<typename T::value_type>>::value ||
std::is_same<T, std::list<typename T::value_type>>::value
>::type* = nullptr>
void CastToJsonElement(JsonElement & ele, const T & value) {
JsonArray array;
for (const typename T::value_type& item : value) {
JsonElement itemElement;
CastToJsonElement<typename T::value_type>(itemElement, item);
array.push_back(itemElement);
}
ele = JsonElement(array);
return;
}
std::map
和std::unordered_map
:
// cast between std::map and JsonElement
template<typename T, typename std::enable_if<
std::is_same<std::string, typename T::key_type>::value && (
std::is_same<T, std::map<std::string, typename T::mapped_type>>::value ||
std::is_same<T, std::unordered_map<std::string, typename T::mapped_type>>::value
)>::type* = nullptr>
void CastFromJsonElement(const JsonElement& ele, T& value) {
JsonObject object = ele.ToJsonObject();
value.clear();
for (const std::pair<std::string, JsonElement>& p : object) {
typename T::mapped_type v;
CastFromJsonElement<typename T::mapped_type>(p.second, v);
value[p.first] = v;
}
return;
}
template<typename T, typename std::enable_if<
std::is_same<std::string, typename T::key_type>::value && (
std::is_same<T, std::map<std::string, typename T::mapped_type>>::value ||
std::is_same<T, std::unordered_map<std::string, typename T::mapped_type>>::value
)>::type* = nullptr>
void CastToJsonElement(JsonElement& ele, const T& value) {
JsonObject object;
for (const std::pair<std::string, typename T::mapped_type>& p : value) {
JsonElement valueElement;
CastToJsonElement<typename T::mapped_type>(valueElement, p.second);
object[p.first] = valueElement;
}
ele = JsonElement(object);
return;
}
到此位置,该JSON库的所有功能都已实现了,来跑一个复杂点的用例:
struct Book
{
std::string m_name;
int m_id;
float m_price;
bool m_soldOut;
std::vector<std::string> m_tags;
SERIALIZE_SECTION_BEGIN
SERIALIZE_FIELD(name, m_name);
SERIALIZE_FIELD(id, m_id);
SERIALIZE_FIELD(price, m_price);
SERIALIZE_FIELD(soldOut, m_soldOut);
SERIALIZE_FIELD(tags, m_tags);
SERIALIZE_SECTION_END
};
struct Author
{
std::string m_name;
std::list<Book> m_books;
SERIALIZE_SECTION_BEGIN
SERIALIZE_FIELD(name, m_name);
SERIALIZE_FIELD(books, m_books);
SERIALIZE_SECTION_END
};
bool operator==(const Book &book1, const Book &book2)
{
return (book1.m_name == book2.m_name &&
book1.m_id == book2.m_id &&
book1.m_price == book2.m_price &&
book1.m_soldOut == book2.m_soldOut &&
book1.m_tags == book2.m_tags);
}
TEST(SerializationTest, NestedStructSerialization)
{
Book book1{};
book1.m_name = "C++ Primer";
book1.m_id = 114514;
book1.m_price = 114.5;
book1.m_soldOut = true;
book1.m_tags = {"C++", "Programming", "Language"};
Book book2{};
book2.m_name = "Essential C++";
book2.m_id = 1919810;
book2.m_price = 19.19;
book2.m_soldOut = false;
book2.m_tags = {"Programming", "Computer Science"};
Author author1{};
author1.m_name = "Stanley B. LippmanBarbara E. Moo JoséeLaJoie";
author1.m_books = {book1, book2};
Author author2{};
std::string jsonStr = util::Serialize(author1);
util::Deserialize(jsonStr, author2);
EXPECT_EQ(author2.m_name, author1.m_name);
EXPECT_EQ(author2.m_books, author1.m_books);
JsonElement ele = JsonParser(jsonStr).Parse();
EXPECT_TRUE(ele.IsJsonObject());
JsonObject authorObject = ele.AsJsonObject();
EXPECT_TRUE(authorObject["name"].IsString());
EXPECT_TRUE(authorObject["books"].IsJsonArray());
JsonArray booksArray = authorObject["books"].AsJsonArray();
EXPECT_EQ(booksArray[0].AsJsonObject()["name"].AsString(), "C++ Primer");
}
完整项目代码见:https://github.com/XUranus/minijson
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK