加载中...
返回

RapidJson使用小记

近期有个需求需要对 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 前使用了 HasMemberIsString() 来进行检查,但假如我们要检查的键变多了,每一次都要使用这样的 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::mapmap 的键就是要检查的键, 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

可以使用 WriterPrettyWriter 来将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::stringint/boolArray 等进行不同的操作,简直是逆天,搞得我特化了三四种模板,基本等于重载 (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.

就可以完成函数的简单测试。

有朋自远方来,不亦说乎?
Built with Hugo
Theme Stack designed by Jimmy