0%

用C++实现一个轻量级Json库

项目地址https://github.com/XUranus/minijson

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

flowchart TD
    JsonElement --> null
    JsonElement --> number
    JsonElement --> bool
    JsonElement --> string
    JsonElement --> JsonObject
    JsonElement --> JsonArray

JsonElement应该提供以下能力:

  1. 记录自己对应的类型信息,并能转化成对应六种的类型之一
  2. 提供统一序列化接口,对每一种类型分别实现序列化

于是首先定义一个序列化接口,所有的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和布尔值的truefalse直接返回对应的字面量即可,数字类型可以用std::to_string直接获得字面量,然后删除末尾冗余的浮点0(详见Json.cppDoubleToString()。字符串序列化需要前后带上引号,需要注意特殊字符的转义。JsonObjectJsonArray两种复合类型在对应自身的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.141145141919.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:

flowchart TD
  obj1(JsonObject) --> objbegin(OBJECT_BEGIN)
  obj1 --> pair1(PAIR_1)
  obj1 --> comma1(COMMA)
  obj1 --> pair2(PAIR_2)
  obj1 --> comma2(COMMA)
  obj1 --> pair3(PAIR_3)
  pair1 --> str1(STRING)
  pair1 --> col1(COLON)
  pair1 --> str2(STRING)
  pair2 --> str3(STRING)
  pair2 --> col2(COLON)
  pair2 --> num1(NUMBER)
  pair3 --> str4(STRING)
  pair3 --> col3(COLON)
  pair3 --> array(JsonArray)
  array --> arraybegin1(ARRAY_BEGIN)
  array --> str5(STRING)
  array --> comma3(COMMA)
  array --> str6(STRING)
  array --> comma4(COMMA)
  array --> str7(STRING)
  array --> arrayend1(ARRAY_END)

明确了原理,接下来就来分别看看JsonScannerJsonParser的实现。

JsonScanner

JsonScanner将在内部存放一个带解析的JSON字符串std::string m_str并维护一个指向其当前读取位置的的下标索引std::size_t m_posJsonScanner将提供一个返回Token的Next()方法供外部调用者调用,返回下一个解析出的Token。

在上述的13种Token种,数字Token(NUMBER)和字符串Token(STRING)解析起来比较复杂,我们将其解析逻辑放入单独的两个解析方法:

double GetNumberValue();
std::string GetStringValue();

他们解析的类型依然由Next()方法返回,但是他们解析出的值需要单独存档在类成员中,可以用std::string m_tmpStrValuedouble m_tmpNumberValue来暂存解析出的值,并由以上两个方法返回。其余的11种Token解析基本拥有固定的字面量(例如LITERAL_TRUE就是trueARRAY_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可能是一个字符串;如果当前字符串是tfn,则下一个Token可能LITERAL_TRUELITERAL_FALSELITERAL_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_BEGINOBJECT_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()方法中,而复杂点的复合结构JsonArrayJsonObject则由独立的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从前往后解析JsonElementJsonParser::ParseNextJsonParser的核心逻辑,他返回下一个解析出的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解析能力直接拿到。而JsonArrayJsonObject的解析较为复杂,则放到单独的ParseJsonObject()ParseJsonArray()方法中实现。

ParseJsonObject()本质上就是解析STRINGCOLON,然后再递归调用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_BEGINSERIALIZE_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)是定义了某一JsonElementT类型直线转化关系的模板函数。后续我们需要针对不同的类型逐一实现他们。

如果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::vectorstd::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::mapstd::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

参考资料

Disqus评论区没有正常加载,请使用科学上网