JSON解析器实现
代码开源仓库:cpp造轮子项目–实现json解析器
JSON格式介绍
JSON(JavaScript Object Notation),是一种序列化的格式,最大的优点在于可读性极强,以及可直接嵌入到js代码中,所以广泛运用于web数据的收发。
JSON格式有以下基本类型:
-
null类型:值为null,表示为空
-
bool类型:值为true和false
-
number类型:值为int、double(即整数或小数
-
string类型:形如 “abc”
以及以下复合类型:
- list类型(也称array类型)
["abc",3.2,323,"sdaf"]
- dict类型(也称object类型)
{
"id":32,
"name":"hhh"
}
解析json字符串
整套解析流程如下:
创建JObject类
我们需要把json的类型对应到计算机语言的类型。
由于json的数据在我们看来都是字符串,那么有如下对应关系:
- “null"对应我们构造的null类型
- “true”,“false"对应内部的bool类型即可
- number类型数据对应int、double类型
- string类型数据对应string即可
- list类型对应C++中的vector
- dict类型对应C++中的map或unordered_map
我们在计算机语言中,需要构造一个对象类型,用于将以上类型全部涵盖。
在C++中我们通过std::variant来进行,还需要一个枚举tag来表示当前对象内存储的数据类型。
当然如果做的更绝的话,可以通过一个void* + 申请堆内存来解决,然后再强转为对应类型来操作。
对应的代码如下:(中间的类方法就暂时省略了
enum TYPE
{
T_NULL,
T_BOOL,
T_INT,
T_DOUBLE,
T_STR,
T_LIST,
T_DICT
};
using null_t = string;
using int_t = int32_t;
using bool_t = bool;
using double_t = double;
using str_t = string;
using list_t = vector<JObject>;
using dict_t = map<string, JObject>;
class JObject
{
public:
using value_t = variant<bool_t, int_t, double_t, str_t, list_t, dict_t>;
...
private:
TYPE m_type;
value_t m_value;
};
创建Parser类
我们有了JObject,可以把所有的JSON数据接收起来,现在要做的就是扫描JSON字符串,对其中的数据进行读取处理,然后转化为JObject。
关键代码如下:
JObject Parser::parse()
{
char token = get_next_token();
if (token == 'n')
{
return parse_null();
}
if (token == 't' || token == 'f')
{
return parse_bool();
}
if (token == '-' || std::isdigit(token))
{
return parse_number();
}
if (token == '\"')
{
return parse_string();
}
if (token == '[')
{
return parse_list();
}
if (token == '{')
{
return parse_dict();
}
throw std::logic_error("unexpected character in parse json");
}
以上就是整个字符串的解析过程,每次通过get_next_token这个方法得到整个字符串的下一个token,根据token决定解析对应的数据类型。
get_next_token方法
跳过空白符号,以及跳过注释(标准的JSON格式不支持注释,我这里硬加的,为了vscode的JSON格式配置文件解析
char Parser::get_next_token()
{
while (std::isspace(m_str[m_idx])) m_idx++;
if (m_idx >= m_str.size())
throw std::logic_error("unexpected character in parse json");
//如果是注释,记得跳过
skip_comment();
return m_str[m_idx];
}
parse_null和parse_bool
由于这两个很简单,就放在一起了。
- parse_null
JObject Parser::parse_null()
{
if (m_str.compare(m_idx, 4, "null") == 0)
{
m_idx += 4;
return {};
}
throw std::logic_error("parse null error");
}
- parse_bool
bool Parser::parse_bool()
{
if (m_str.compare(m_idx, 4, "true") == 0)
{
m_idx += 4;
return "true";
}
if (m_str.compare(m_idx, 5, "false") == 0)
{
m_idx += 5;
return "false";
}
throw std::logic_error("parse bool error");
}
parse_number
JObject Parser::parse_number()
{
auto pos = m_idx;
//integer part
if (m_str[m_idx] == '-')
{
m_idx++;
}
if (isdigit(m_str[m_idx]))
while (isdigit(m_str[m_idx]))
m_idx++;
else
{
throw std::logic_error("invalid character in number");
}
if (m_str[m_idx] != '.')
{
return (int) strtol(m_str.c_str() + pos, nullptr, 10);
}
//decimal part
if (m_str[m_idx] == '.')
{
m_idx++;
if (!std::isdigit(m_str[m_idx]))
{
throw std::logic_error("at least one digit required in parse float part!");
}
while (std::isdigit(m_str[m_idx]))
m_idx++;
}
return strtof64(m_str.c_str() + pos, nullptr);
}
parse_list
JObject Parser::parse_list()
{
JObject arr((list_t()));//得到list类型的JObject
m_idx++;
char ch = get_next_token();
if (ch == ']')
{
m_idx++;
return arr;
}
while (true)
{
arr.push_back(parse());
ch = get_next_token();
if (ch == ']')
{
m_idx++;
break;
}
if (ch != ',') //如果不是逗号
{
throw std::logic_error("expected ',' in parse list");
}
//跳过逗号
m_idx++;
}
return arr;
}
parse_dict
JObject Parser::parse_dict()
{
JObject dict((dict_t()));//得到dict类型的JObject
m_idx++;
char ch = get_next_token();
if (ch == '}')
{
m_idx++;
return dict;
}
while (true)
{
//解析key
string key = std::move(parse().Value<string>());
ch = get_next_token();
if (ch != ':')
{
throw std::logic_error("expected ':' in parse dict");
}
m_idx++;
//解析value
dict[key] = parse();
ch = get_next_token();
if (ch == '}')
{
m_idx++;
break; //解析完毕
}
if (ch != ',')//没有结束,此时必须为逗号
{
throw std::logic_error("expected ',' in parse dict");
}
//跳过逗号
m_idx++;
}
return dict;
}
完善JObject类
很明显,我们需要为JObject类提供一个方法,此方法可以让调用者直接访问到std::variant里面对应的数据,并且我们也需要提供一个方法能让JObject快速初始化为对应的类型。
初始化接口
添加好下面这些方法后,外界可通过调用方法把JObject的内部状态改变。
void Null()
{
m_type = T_NULL;
m_value = "null";
}
void Int(int_t value)
{
m_value = value;
m_type = T_INT;
}
void Bool(bool_t value)
{
m_value = value;
m_type = T_BOOL;
}
void Double(double_t value)
{
m_type = T_DOUBLE;
m_value = value;
}
void Str(string_view value)
{
m_value = string(value);
m_type = T_STR;
}
void List(list_t value)
{
m_value = std::move(value);
m_type = T_LIST;
}
void Dict(dict_t value)
{
m_value = std::move(value);
m_type = T_DICT;
}
为了方便平时的赋值时候的隐式转化,我们应该再添加对应的构造函数,如下:(隐式转化在C++里有个坑,只能为类提供一种方向的隐式转化,比如提供了int把转为JObject的隐式转化后,就不能再提供把JObject转为int的隐式转化了,这两种必须要有一个是explicit,否则报错
JObject()//默认为null类型
{
m_type = T_NULL;
m_value = "null";
}
JObject(int_t value)
{
Int(value);
}
JObject(bool_t value)
{
Bool(value);
}
JObject(double_t value)
{
Double(value);
}
JObject(str_t const &value)
{
Str(value);
}
JObject(list_t value)
{
List(std::move(value));
}
JObject(dict_t value)
{
Dict(std::move(value));
}
请求值接口
设计思路:本人不喜欢一大堆的get/set,那样真的是麻烦。我通过提供一个Value方法,该方法为泛型,其内部有调用value()方法得到对应的数据指针,而Value方法则负责将指针强转,其内部也实现了强大的错误处理,防止处理指针的意外宕机。。。
value方法如下,用于调用get_if得到对应的数据指针,关于怎么获取std::variant的数据,有get和get_if两种方式,get得到的是对象的引用,如果获取不到,则抛出异常,get_if获取对象的指针,如果获取不到则返回nullptr。我选择使用get_if的原因是,这个异常的处理可以由你自己来设定提示,而不是对着底层的get提示而摸不着头脑。
void *JObject::value()
{
switch (m_type)
{
case T_NULL:
return get_if<str_t>(&m_value);
case T_BOOL:
return get_if<bool_t>(&m_value);
case T_INT:
return get_if<int_t>(&m_value);
case T_DOUBLE:
return get_if<double_t>(&m_value);
case T_LIST:
return get_if<list_t>(&m_value);
case T_DICT:
return get_if<dict_t>(&m_value);
case T_STR:
return std::get_if<str_t>(&m_value);
default:
return nullptr;
}
}
Value方法:
#define THROW_GET_ERROR(erron) throw std::logic_error("type error in get "#erron" value!")
template<class V>
V &Value()
{
//添加安全检查
if constexpr(IS_TYPE(V, str_t))
{
if (m_type != T_STR)
THROW_GET_ERROR(string);
} else if constexpr(IS_TYPE(V, bool_t))
{
if (m_type != T_BOOL)
THROW_GET_ERROR(BOOL);
} else if constexpr(IS_TYPE(V, int_t))
{
if (m_type != T_INT)
THROW_GET_ERROR(INT);
} else if constexpr(IS_TYPE(V, double_t))
{
if (m_type != T_DOUBLE)
THROW_GET_ERROR(DOUBLE);
} else if constexpr(IS_TYPE(V, list_t))
{
if (m_type != T_LIST)
THROW_GET_ERROR(LIST);
} else if constexpr(IS_TYPE(V, dict_t))
{
if (m_type != T_DICT)
THROW_GET_ERROR(DICT);
}
void *v = value();
if (v == nullptr)
throw std::logic_error("unknown type in JObject::Value()");
return *((V *) v); //强转即可
}
重载方法让对象更好用
当JObject为dict类型时,我们可以直接用下标运算符进行key-value的赋值(得益于隐式转化和运算符重载
JObject &operator[](string const &key)
{
if (m_type == T_DICT)
{
auto &dict = Value<dict_t>();
return dict[key];
}
throw std::logic_error("not dict type! JObject::opertor[]()");
}
同样如果为list对象,我们也准备了push_back等方法
void push_back(JObject item)
{
if (m_type == T_LIST)
{
auto &list = Value<list_t>();
list.push_back(std::move(item));
return;
}
throw std::logic_error("not a list type! JObjcct::push_back()");
}
当然,为了让类更好用,你也可以重载很多其他方法,但是注意别忘了类型的安全检查!
完善Parser类
我们之前是完成了整个字符串到JObject的解析过程,但是每次都需要创建一个Parser类,然后调用方法,这样的过程未免有些繁琐,我们可以对外提供FromString的静态方法,然后充分利用一个对象便可完成整个解析过程。
如下:
JObject Parser::FromString(string_view content)
{
static Parser instance;
instance.init(content);
return instance.parse();
}
完成序列化和反序列化过程
我们前面所做的工作其实已经把这个过程相当于完成了,当然还差一个把JObject变为JSON字符串的方法,现在添加上就是,也不是很难,按照相似的逻辑反推一遍就行。如下给JObject添加一个to_string方法:
string JObject::to_string()
{
void *value = this->value();
std::ostringstream os;
switch (m_type)
{
case T_NULL:
os << "null";
break;
case T_BOOL:
if (GET_VALUE(bool))
os << "true";
else os << "false";
break;
case T_INT:
os << GET_VALUE(int);
break;
case T_DOUBLE:
os << GET_VALUE(double);
break;
case T_STR:
os << '\"' << GET_VALUE(string) << '\"';
break;
case T_LIST:
{
list_t &list = GET_VALUE(list_t);
os << '[';
for (auto i = 0; i < list.size(); i++)
{
if (i != list.size() - 1)
{
os << ((list[i]).to_string());
os << ',';
} else os << ((list[i]).to_string());
}
os << ']';
break;
}
case T_DICT:
{
dict_t &dict = GET_VALUE(dict_t);
os << '{';
for (auto it = dict.begin(); it != dict.end(); ++it)
{
if (it != dict.begin()) //为了保证最后的json格式正确
os << ',';
os << '\"' << it->first << "\":" << it->second.to_string();
}
os << '}';
break;
}
default:
return "";
}
return os.str();
}
有关JObject的方法也都补充的差不多了,那么我们现在要考虑的是如何通过JObject这个中间对象将我们自定义的任何一个类给序列化和反序列化?
如图:
所有的序列化和反序列化的过程都依托于JObject进行。而Parser这个类在中间作为一个方便使用的对外接口。
序列化接口设计
在Parser类中添加一个ToJSON的静态方法,用来对任意类型进行序列化,这个方法使用模板。
代码如下:
template<class T>
static string ToJSON(T const &src)
{
//如果是基本类型
if constexpr(IS_TYPE(T, int_t))
{
JObject object(src);
return object.to_string();
} else if constexpr(IS_TYPE(T, bool_t))
{
JObject object(src);
return object.to_string();
} else if constexpr(IS_TYPE(T, double_t))
{
JObject object(src);
return object.to_string();
} else if constexpr(IS_TYPE(T, str_t))
{
JObject object(src);
return object.to_string();
}
//如果是自定义类型调用方法完成dict的赋值,然后to_string即可
json::JObject obj((json::dict_t()));
src.FUNC_TO_NAME(obj);
return obj.to_string();
}
如上代码如果是基本类型则直接初始化JObject调用to_string方法,如果是自定义类型,则需要在该类型中嵌入一个方法,这个方法名字我们用FUNC_TO_NAME这个宏代替。
也就是说,如果是自定义的类型,那么肯定就是对应JSON数据的dict类型,所以只需要你对该类型定义对应的方法,该方法需要将值传递给JObject,为了简化这个过程我们用宏来替代。
宏定义如下:
#define FUNC_TO_NAME _to_json
#define FUNC_FROM_NAME _from_json
#define START_TO_JSON void FUNC_TO_NAME(json::JObject & obj) const{
#define to(key) obj[key]
//push一个自定义类型的成员
#define to_struct(key, struct_member) json::JObject tmp((json::dict_t())); struct_member.FUNC_TO_NAME(tmp); obj[key] = tmp
#define END_TO_JSON }
#define START_FROM_JSON void FUNC_FROM_NAME(json::JObject& obj) {
#define from(key, type) obj[key].Value<type>()
#define from_struct(key, struct_member) struct_member.FUNC_FROM_NAME(obj[key])
#define END_FROM_JSON }
基本使用如下:
struct Base
{
int pp;
string qq;
START_FROM_JSON //生成反序列化相关的方法
pp = from("pp", int);
qq = from("qq", string);
END_FROM_JSON
START_TO_JSON //生成序列化相关的代码
to("pp") = pp;
to("qq") = qq;
END_TO_JSON
};
struct Mytest
{
int id;
std::string name;
Base q;
START_TO_JSON //序列化相关代码
to_struct("base", q);
to("id") = id;
to("name") = name;
END_TO_JSON
START_FROM_JSON //反序列化相关代码
id = from("id", int);
name = from("name", string);
from_struct("base", q);
END_FROM_JSON
};
实际上上面代码等效于下面的代码,以Base类为例:
struct Base
{
int pp;
string qq;
void _from_json(json::JObject& obj){ //反序列化
pp = obj.Value<int>();
qq = obj.Value<string>();
}
void _to_json(json::JObject& obj){//序列化代码
obj["pp"] = pp;
obj["qq"] = qq;
}
};
简单使用
#include<iostream>
#include "JObject.h"
#include "Parser.h"
#include <fstream>
#include "../benchmark/Timer.hpp"
using namespace json;
struct Base
{
int pp;
string qq;
START_FROM_JSON
pp = from("pp", int);
qq = from("qq", string);
END_FROM_JSON
START_TO_JSON
to("pp") = pp;
to("qq") = qq;
END_TO_JSON
};
struct Mytest
{
int id;
std::string name;
Base q;
START_TO_JSON
to_struct("base", q);
to("id") = id;
to("name") = name;
END_TO_JSON
START_FROM_JSON
id = from("id", int);
name = from("name", string);
from_struct("base", q);
END_FROM_JSON
};
void test_class_serialization()
{
Mytest test{.id=32, .name="fda"};
auto item = Parser::FromJson<Mytest>(R"({"base":{"pp":0,"qq":""},"id":32,"name":"fda"} )"); //serialization
std::cout << Parser::ToJSON(item); //deserialization
}
void test_string_parser()
{
{
Timer t;
std::ifstream fin("../../test_source/test.json");
std::string text((std::istreambuf_iterator<char>(fin)), std::istreambuf_iterator<char>());
json::Parser p;
p.init(text);
auto q = p.parse();
std::ofstream fout("../../test_source/test_out.json");
fout << q.to_string();
}
}
int main()
{
test_class_serialization();
test_string_parser();
}