近期有个需求需要对 JSON
数据进行解析,上周五搞了一点点之后就去忙其他的事情了,正好用周末时间搞一搞,输出点东西。
RapidJson 是一个C++的JSON解析器及生成器,特点详见官网,此处仅作使用记录。
环境:
Windows 11 64bit
WSL2 Ubuntu 22.04
cmake version 3.22.1
gcc version 11.2.0 (Ubuntu 11.2.0-19ubuntu1)
读者需要具备的一些前置知识:
cmake基本使用、c++、Linux基本使用
Step 1 安装
RapidJson 项目是一个纯头文件库,直接把仓库中的 include/rapidjson
拷贝到我们CPP项目的头文件包含路径内即可。
Step 1.1 cmake install
对于cmake项目来说,只需要创建一个合适的地方来存放头文件:
albusguo@AlbusGuo-PC:/mnt/d/Scripts/CPPScripts/RapidJson$ tree
.
├── CMakeLists.txt
├── build
├── include
│ └── rapidjson
│ ├── allocators.h
│ ├── cursorstreamwrapper.h
│ ├── document.h
│ ├── encodedstream.h
│ ├── encodings.h
│ ├── error
│ │ ├── en.h
│ │ └── error.h
│ ├── filereadstream.h
│ ├── filewritestream.h
│ ├── fwd.h
│ ├── internal
│ │ ├── biginteger.h
│ │ ├── clzll.h
│ │ ├── diyfp.h
│ │ ├── dtoa.h
│ │ ├── ieee754.h
│ │ ├── itoa.h
│ │ ├── meta.h
│ │ ├── pow10.h
│ │ ├── regex.h
│ │ ├── stack.h
│ │ ├── strfunc.h
│ │ ├── strtod.h
│ │ └── swap.h
│ ├── istreamwrapper.h
│ ├── memorybuffer.h
│ ├── memorystream.h
│ ├── msinttypes
│ │ ├── inttypes.h
│ │ └── stdint.h
│ ├── ostreamwrapper.h
│ ├── pointer.h
│ ├── prettywriter.h
│ ├── rapidjson.h
│ ├── reader.h
│ ├── schema.h
│ ├── stream.h
│ ├── stringbuffer.h
│ ├── uri.h
│ └── writer.h
└── src
├── CMakeLists.txt
├── core
├── main
└── main.cpp
然后在 src/CMakeLists.txt
文件里指定 INCLUDE_DIRECTORIES(../include)
即可。
Step 1.2 VSCode install
按照上一步完成配置,则cmake可以正确地完成编译工作;但我们通常在VSCode里进行编码,也有部分读者习惯通过VSCode进行运行/调试,因此有必要对VSCode也进行配置。
按 F1
调出指令框,输入 c/c++: Edit Configurations
打开 C/C++
插件的JSON配置文件:
指定 includePath
:
这样,编码时就能完成头文件的查找和自动补全了。
紧接着 ,我们要配置一键编译运行的查找路径。VSCode编码时使用的头文件路径和编译时使用的头文件路径并不共享,我们单击右上角的小三角来一键运行时,本质上是快捷执行了一条终端命令而已,因此要对这条命令来进行额外配置。
在工作区根目录下找到 .vscode
文件夹,打开 tasks.json
:
如果不存在这个文件夹或不存在这个文件,可以自己新建。
tasks.json
文件当中有一键运行的配置项,我们在编译参数里面新增 -I
来指定包含路径,这里贴出我的 tasks.json
,供参考:
{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++ build active file",
"command": "/usr/bin/g++",
"args": [
"-fdiagnostics-color=always",
"-g",
"${file}",
"-o",
"${fileDirname}/${fileBasenameNoExtension}",
"-I${workspaceFolder}/RapidJson/include",
"-I${workspaceFolder}/RapidJson/third/googletest/include"
],
"options": {
"cwd": "${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Task generated by Debugger."
}
],
"version": "2.0.0"
}
完成之后,就可以一键运行/调试我们的cpp文件了。
Step 2 使用
Step 2.1 读JSON
RapidJson将一个JSON字符串解析成一个文档对象模型(Document Object Model,DOM),类似Javascript对HTML页面的操作。
Talk is cheap :
#include <rapidjson/document.h>
#include <string>
#include <iostream>
using namespace rapidjson;
int main(int argc, char *atgv[])
{
std::string jsonStr =
"{\n"
" \"m1\": \"afcdeb1238\",\n"
" \"m2\": \"1234567675\",\n"
" \"m3\": \"sdasdqwsad\",\n"
" \"list\": [\n"
" {\n"
" \"afafa\": \"alkkk\",\n"
" \"abcdf\": \"asdqw\"\n"
" }\n"
" ]\n"
"}";
// 解析JSON字符串
Document doc;
if (doc.Parse(jsonStr.c_str()).HasParseError()) {
doc = NULL;
}
// ==== snip ====
return 0;
}
上面这段代码中,我们对一个JSON字符串进行了解析,按照 官方文档 的说法,我们得到的是一个 Object
对象,它是DOM树的根节点:
我们可以针对一个节点进行一些操作:
#include <rapidjson/document.h>
#include <string>
#include <iostream>
using namespace rapidjson;
int main(int argc, char *atgv[])
{
std::string jsonStr =
"{\n"
" \"m1\": \"afcdeb1238\",\n"
" \"m2\": \"1234567675\",\n"
" \"m3\": \"sdasdqwsad\",\n"
" \"list\": [\n"
" {\n"
" \"afafa\": \"alkkk\",\n"
" \"abcdf\": \"asdqw\"\n"
" }\n"
" ]\n"
"}";
// 解析JSON字符串
Document doc;
if (doc.Parse(jsonStr.c_str()).HasParseError()) {
doc = NULL;
}
// 输出member
if (doc.HasMember("m1") && doc["m1"].IsString()) {
std::cout << doc["m1"].GetString() << std::endl;
}
// ==== snip ====
return 0;
}
cd build
cmake ..
make
./test_rapid_json
# afcdeb1238
Step 2.2 检查JSON合法性(lambda)
通常在业务中收到JSON数据时,可能由于这样那样的原因,少掉了一些键,或者键的类型不对。例如我们在上一步通过 doc["m1"].GetString()
取出了 m1
的值,转化成了 std::string
进行输出,但如果 m1
不存在呢?或者 m1
不是个字符串、而是个列表呢?因此在这一步,我们要对解析后的JSON对象的合法性进行检查。
对于一个RapidJson对象,可以使用 HasMember()
和 Isxxxx()
来检查对象的合法性,例如上面的代码在输出 m1
前使用了 HasMember
和 IsString()
来进行检查,但假如我们要检查的键变多了,每一次都要使用这样的 if
语句来进行判断,代码非常不简洁。
检查函数的目标是,可以一次性完成合法性检验,我们把要检查的键和类型传入,函数完成检查;只要某一个键不存在、或某一个键的类型有问题,就说明JSON数据有误,返回 false
。
我实现的代码如下:
class JsonObj {
public:
explicit JsonObj(const char *jsonStr) {
if (doc.Parse(jsonStr).HasParseError()) {
doc = NULL;
}
}
bool IsNullDoc() { return doc.IsNull(); }
bool Validation(const std::map<std::string, std::function<bool(const Value&)>> valid) const;
private:
Document doc;
};
bool JsonObj::Validation(const std::map<std::string, std::function<bool(const Value&)>> valid) const
{
if (doc.IsNull()) {
return false;
}
if (valid.size() == 0) {
return true;
}
for (auto iter : valid) {
if (doc.HasMember(iter.first.c_str())) {
std::cout << iter.first << " passed existance check.\n";
} else {
std::cout << iter.first << " failed existance check.\n";
return false;
}
const Value &memberObj = doc[iter.first.c_str()];
if (iter.second(memberObj)) {
std::cout << iter.first << " passed type check." << std::endl;
} else {
std::cout << iter.first << " failed type check." << std::endl;
return false;
}
}
return true;
}
int main()
{
std::string jsonStr =
"{\n"
" \"m1\": \"afcdeb1238\",\n"
" \"m2\": \"1234567675\",\n"
" \"m3\": \"sdasdqwsad\",\n"
" \"list\": [\n"
" {\n"
" \"afafa\": \"alkkk\",\n"
" \"abcdf\": \"asdqw\"\n"
" }\n"
" ]\n"
"}";
// std::cout << jsonStr << std::endl;
JsonObj jobj(jsonStr.c_str());
std::map<std::string, std::function<bool(const Value&)>> valid = {
{"m1", [](const Value &obj) -> bool { return obj.IsString(); }},
{"m2", [](const Value &obj) -> bool { return obj.IsString(); }},
{"list", [](const Value &obj) -> bool { return obj.IsArray(); }}
};
jobj.Validation(valid);
return 0;
}
函数接收一个 std::map
, map
的键就是要检查的键, map
的值是这个键对应的类型检查函数,类型是 std::function
;例如对于键 m1
要调用 IsString()
,而对于键 list
要调用 IsArray()
。在构造这个 map
的时候,可以使用lambda表达式来作为 std::function
的对象,函数内部将自动调用对应的lambda表达式来检查类型。
这个实现谈不上优雅,或者简直是有点挫,因为在编写这些代码的时候还不熟悉RapidJson的其他API;但可以算作一次积极的尝试,也算是对 std::function
和lambda表达式的一次练习。
Step 2.3 检查JSON合法性(GetType)
在官方文档 查询Object 一栏中,发现了RapidJson对于对象类型的定义和查询方法:
static const char* kTypeNames[] =
{ "Null", "False", "True", "Object", "Array", "String", "Number" };
for (Value::ConstMemberIterator itr = document.MemberBegin();
itr != document.MemberEnd(); ++itr)
{
printf("Type of member %s is %s\n",
itr->name.GetString(), kTypeNames[itr->value.GetType()]);
}
Type of member hello is String
Type of member t is True
Type of member f is False
Type of member n is Null
Type of member i is Number
Type of member pi is Number
Type of member a is Array
可以看到,成员函数 GetType
返回的是这个对象的类型ID,而类型的名称就按照这里的 kTypeNames
的顺序定义。
这是非常好的,对于我们的检查函数来说,我们也可以按照这样的顺序来把类型名称固定下来,检查的时候,只要检查类型名称就可以,大大提高了可读性:
class JsonObj {
public:
explicit JsonObj(const char *jsonStr) {
if (doc.Parse(jsonStr).HasParseError()) {
doc = NULL;
}
}
bool IsNullDoc() { return doc.IsNull(); }
bool Validation(const std::map<std::string, std::string> valid) const;
private:
Document doc;
std::vector<std::string> kTypeNames = {
"Null",
"False",
"True",
"Object",
"Array",
"String",
"Number"
};
};
bool JsonObj::Validation(const std::map<std::string, std::string> valid) const
{
if (doc.IsNull()) {
return false;
}
if (valid.size() == 0) {
return true;
}
for (auto iter : valid) {
if (doc.HasMember(iter.first.c_str())) {
std::cout << iter.first << " passed existance check.\n";
} else {
std::cout << iter.first << " failed existance check.\n";
return false;
}
auto typeId = doc[iter.first.c_str()].GetType();
if (kTypeNames[typeId] == iter.second) {
std::cout << iter.first << " passed type check." << std::endl;
} else {
std::cout << iter.first << " failed type check.\n";
std::cout << "real type: " << kTypeNames[typeId] << std::endl;
return false;
}
}
return true;
}
bool JsonObj::Validation(const std::map<std::string, std::string> valid) const
{
if (doc.IsNull()) {
return false;
}
if (valid.size() == 0) {
return true;
}
for (auto iter : valid) {
if (doc.HasMember(iter.first.c_str())) {
std::cout << iter.first << " passed existance check.\n";
} else {
std::cout << iter.first << " failed existance check.\n";
return false;
}
auto typeId = doc[iter.first.c_str()].GetType();
if (kTypeNames[typeId] == iter.second) {
std::cout << iter.first << " passed type check." << std::endl;
} else {
std::cout << iter.first << " failed type check.\n";
std::cout << "real type: " << kTypeNames[typeId] << std::endl;
return false;
}
}
return true;
}
int main()
{
std::string jsonStr =
"{\n"
" \"m1\": \"afcdeb1238\",\n"
" \"m2\": \"1234567675\",\n"
" \"m3\": \"sdasdqwsad\",\n"
" \"list\": [\n"
" {\n"
" \"afafa\": \"alkkk\",\n"
" \"abcdf\": \"asdqw\"\n"
" }\n"
" ]\n"
"}";
// std::cout << jsonStr << std::endl;
JsonObj jobj(jsonStr.c_str());
std::map<std::string, std::function<bool(const Value&)>> valid = {
{"m1", "String"},
{"m2", "String"},
{"list", "Array"}
};
jobj.Validation(valid);
return 0;
由于类型检查本质上是在检查它的数值形式的 typeId
,则我们也可以把类型名称从字符串形式优化成枚举值的形式,节约存储空间和比较的时间,在这里就不多写了。
Step 2.4 写JSON
可以使用 Writer
或 PrettyWriter
来将DOM对象转化成JSON串。
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/prettywriter.h"
#include "rapidjson/stringbuffer.h"
#include "gtest/gtest.h"
#include <map>
#include <string>
#include <iostream>
#include <functional>
#include <vector>
using namespace rapidjson;
namespace {
class JsonObj {
public:
JsonObj() { doc.SetObject(); }
explicit JsonObj(const char *jsonStr) {
if (doc.Parse(jsonStr).HasParseError()) {
doc = NULL;
}
}
template<typename T>
bool AddMember(const std::string &key, const T &value);
bool PrintJsonStr();
const Value& GetObj() { return doc.GetObject(); }
private:
Document doc;
};
template<>
bool JsonObj::AddMember(const std::string &key, const std::string &value)
{
if (!doc.IsObject()) {
return false;
}
auto &allocator = doc.GetAllocator();
Value memKey(key.c_str(), allocator);
Value memVal(value.c_str(), allocator);
doc.AddMember(memKey, memVal, allocator);
return true;
}
template<>
bool JsonObj::AddMember(const std::string &key, const int &value)
{
if (!doc.IsObject()) {
return false;
}
auto &allocator = doc.GetAllocator();
Value memKey(key.c_str(), allocator);
doc.AddMember(memKey, value, allocator);
return true;
}
template<>
bool JsonObj::AddMember(const std::string &key, const bool &value)
{
if (!doc.IsObject()) {
return false;
}
auto &allocator = doc.GetAllocator();
Value memKey(key.c_str(), allocator);
doc.AddMember(memKey, value, allocator);
return true;
}
template<typename T>
bool JsonObj::AddMember(const std::string &key, const T &value)
{
if (!doc.IsObject()) {
return false;
}
auto &allocator = doc.GetAllocator();
Value memKey(key.c_str(), allocator);
Value memVal(value, allocator);
doc.AddMember(memKey, memVal, allocator);
return true;
}
bool JsonObj::PrintJsonStr()
{
if (!doc.IsObject()) {
return false;
}
StringBuffer buffer;
// Writer<StringBuffer> writer(buffer);
PrettyWriter<StringBuffer> writer(buffer);
doc.Accept(writer);
std::cout << buffer.GetString() << std::endl;
return true;
}
class RapidJsonTest : public testing::Test {
protected:
void SetUp() override {}
void TearDown() override {}
};
TEST_F(RapidJsonTest, TestValidation)
{
JsonObj obj;
Document doc;
auto allocator = doc.GetAllocator();
bool res;
res = obj.AddMember<std::string>("app_id", "00");
EXPECT_TRUE(res);
res = obj.AddMember<std::string>("func", "import");
EXPECT_TRUE(res);
res = obj.AddMember<int>("num", 100);
EXPECT_TRUE(res);
Value vArray(kArrayType);
for (int i = 0; i < 3; ++i) {
Value tmp(kObjectType);
tmp.AddMember("m4", i, allocator);
tmp.AddMember("m5", i + 10, allocator);
vArray.PushBack(tmp, allocator);
}
res = obj.AddMember<Value>("obj_list", vArray);
EXPECT_TRUE(res);
obj.PrintJsonStr();
}
int main()
{
testing::InitGoogleTest();
return RUN_ALL_TESTS();
}
}
albusguo@AlbusGuo-PC:/mnt/d/Scripts/CPPScripts/RapidJson/build$ ./test_rapid_json
Running main() from /mnt/d/Scripts/CPPScripts/RapidJson/third/googletest/src/gtest_main.cc
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from RapidJsonTest
[ RUN ] RapidJsonTest.TestValidation
{
"app_id": "00",
"func": "import",
"num": 100,
"obj_list": [
{
"m4": 0,
"m5": 10
},
{
"m4": 1,
"m5": 11
},
{
"m4": 2,
"m5": 12
}
]
}
[ OK ] RapidJsonTest.TestValidation (0 ms)
[----------] 1 test from RapidJsonTest (0 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
在上面的代码中,对 Object
额外封装了一个模板成员函数 AddMember
来添加JSON键值,但实际操作起来之后发现RapidJson在创建 Value
的时候居然要对 std::string
、 int/bool
、Array
等进行不同的操作,简直是逆天,搞得我特化了三四种模板,基本等于重载 (lll¬ω¬) 。
不过考虑到明天上班可能还需要再参考一下今天写出来的代码,就先放到博客上吧,这部分有一点点烂尾,见谅。
番外:RapidJson In GTest
在 开发者测试 的概念中,编码时不仅要实现业务功能,还要编写有相应的测试用例。GTest 就是一个很好的测试框架,可以帮助我们快速地完成用例编写。
可以直接把源码拉下来,然后把仓库中 googletest
文件夹复制到cmake项目中的任意位置。
在根目录的 CMakeLists.txt
中把GTest源码包含进项目中:
PROJECT(TRY_RAPID_JSON)
# GoogleTest requires at least C++14
set(CMAKE_CXX_STANDARD 14)
set(GOOGLETEST_VERSION 1.12.1)
ADD_SUBDIRECTORY(third/googletest .)
ADD_SUBDIRECTORY(src .)
在源码目录的 CMakeLists.txt
中把GTest库连接到我们的程序里:
PROJECT(TRY_RAPID_JSON)
INCLUDE_DIRECTORIES(../include)
ADD_EXECUTABLE(test_rapid_json main.cpp)
TARGET_LINK_LIBRARIES(
test_rapid_json
GTest::gtest_main
)
之后就可以愉快地使用GTest了。
对于VSCode的头文件的包含,可以参考 Step 1.2 VSCode install ,不过一键运行我没有配置,目前还是有bug的。
在 main.cpp
里面编写测试用例:
#include <rapidjson/document.h>
#include "gtest/gtest.h"
#include <map>
#include <string>
#include <iostream>
#include <functional>
#include <vector>
using namespace rapidjson;
namespace {
class JsonObj {
public:
explicit JsonObj(const char *jsonStr) {
if (doc.Parse(jsonStr).HasParseError()) {
doc = NULL;
}
}
bool IsNullDoc() { return doc.IsNull(); }
bool Validation(const std::map<std::string, std::string> valid) const;
private:
Document doc;
std::vector<std::string> kTypeNames = {
"Null",
"False",
"True",
"Object",
"Array",
"String",
"Number"
};
};
bool JsonObj::Validation(const std::map<std::string, std::string> valid) const
{
if (doc.IsNull()) {
return false;
}
if (valid.size() == 0) {
return true;
}
for (auto iter : valid) {
if (doc.HasMember(iter.first.c_str())) {
std::cout << iter.first << " passed existance check.\n";
} else {
std::cout << iter.first << " failed existance check.\n";
return false;
}
auto typeId = doc[iter.first.c_str()].GetType();
if (kTypeNames[typeId] == iter.second) {
std::cout << iter.first << " passed type check." << std::endl;
} else {
std::cout << iter.first << " failed type check.\n";
std::cout << "real type: " << kTypeNames[typeId] << std::endl;
return false;
}
}
return true;
}
class RapidJsonTest : public testing::Test {
protected:
void SetUp() override {
jsonStr =
"{\n"
" \"m1\": \"ab123\",\n"
" \"m2\": \"1234567675\",\n"
" \"m3\": \"sdasdqwsad\",\n"
" \"list\": [\n"
" {\n"
" \"afafa\": \"alkkk\",\n"
" \"abcdf\": \"asdqw\"\n"
" }\n"
" ]\n"
"}";
invalidJsonStr =
"{\n"
" \"m1\": \"afcdeb1238\",\n"
" \"m2\": \"1234567675\",\n"
" \"m3\": \"sdasdqwsad\",\n"
" \"list\": [\n"
"}";
}
std::string jsonStr;
std::string invalidJsonStr;
};
TEST_F(RapidJsonTest, TestValidation)
{
// std::cout << jsonStr << std::endl;
JsonObj jobj(jsonStr.c_str());
JsonObj invalidJobj(invalidJsonStr.c_str());
std::map<std::string, std::string> valid = {
{"m1", "String"},
{"m2", "String"},
{"list", "Array"}
};
EXPECT_FALSE(jobj.IsNullDoc());
EXPECT_TRUE(jobj.Validation(valid));
EXPECT_TRUE(invalidJobj.IsNullDoc());
EXPECT_FALSE(invalidJobj.Validation(valid));
}
int main()
{
testing::InitGoogleTest();
return RUN_ALL_TESTS();
}
}
albusguo@AlbusGuo-PC:/mnt/d/Scripts/CPPScripts/RapidJson/build$ ./test_rapid_json
Running main() from /mnt/d/Scripts/CPPScripts/RapidJson/third/googletest/src/gtest_main.cc
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from RapidJsonTest
[ RUN ] RapidJsonTest.TestValidation
list passed existance check.
list passed type check.
m1 passed existance check.
m1 passed type check.
m2 passed existance check.
m2 passed type check.
[ OK ] RapidJsonTest.TestValidation (0 ms)
[----------] 1 test from RapidJsonTest (0 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[ PASSED ] 1 test.
就可以完成函数的简单测试。