c++のrapidjsonでjsonを読み込む

# はじめに
C++でjsonを扱う方法を調べたのでまとめました。
開発では、C++とpythonを連携して、jsonの解析などはライブラリの豊富なpythonで行い、計算は処理の早いC++で行う方法が開発しやすいですが、
速度を意識する開発の場合、C++ですべてを行いたい場合があります。そのような時に使えるのがこのライブラリです。

# 環境
 - ubuntu:18.04.2
 - g++:7.4.0

# インストール
インストールは```apt install rapidjson-dev```だけです。ヘッダオンリーのライブラリなのでインストール後のコンパイルでは特に何もせずに使用できます。今回の例でいうと``` g++ cppMain.cpp -o cppMain ```でコンパイルしています。

# rapidjsonの使い方
## json形式のcharの解析
### json形式のcharの解析の使用例
json形式のchar(sample_json)をrapidjsonのDocumentに読み込ませて値を取得して標準出力をしています。


``` cppMain.cpp
#include "rapidjson/document.h"
#include "rapidjson/error/en.h"
#include <iostream>

static const char* sample_json = R"(
{
    "pets": "dog",
    "age": 5,
    "like": [
        "walking",
        "eating",
        "hamster"
    ]
}
)";

int main(){
    rapidjson::Document doc;
    doc.Parse(sample_json);
    if(doc.HasParseError()) {
        std::cout << "error offset:" << doc.GetErrorOffset() << std::endl;
        std::cout << "error pase:" << rapidjson::GetParseError_En(doc.GetParseError()) << std::endl;
        return 1;        
    }

    std::cout << "---- string ----" << std::endl;
    if(!doc["pets"].IsString()) {
        std::cout << "pets value is not string" << std::endl;
        return 1;
    }
    const char* a = doc["pets"].GetString();
    std::cout << "pets value:" << a << std::endl;

    std::cout << "---- list ----" << std::endl;
    const rapidjson::Value& c = doc["like"].GetArray();

    for (auto& d : c.GetArray()) {
        const char* e = d.GetString();
        std::cout << "list value:" << e << std::endl;
    }

    std::cout << "---- key list ----" << std::endl;
    for(rapidjson::Value::ConstMemberIterator itr = doc.MemberBegin(); itr != doc.MemberEnd(); ++itr) {
            std::cout << "key name:" << itr->name.GetString() << std::endl;
    }

    return 0;
}
```

### json形式のcharの解析の説明
#### jsonの解析
まず、rapidjsonのDocumentを宣言しています。このDocumentを主に使用してjsonの解析や値の取得などを行っていきます。次にDocumentのParse関数にsample_jsonを渡して解析しています。
その後、HasParseError関数で解析でエラーしたかを判断しています。
エラーが出た場合はGetErrorOffsetでエラーの位置、rapidjson::GetParseError_En(doc.GetParseError())でエラーの内容を取得しています。


``` cppMain.cpp
    rapidjson::Document doc;
    doc.Parse(sample_json);
    if(doc.HasParseError()) {
        std::cout << "error offset:" << doc.GetErrorOffset() << std::endl;
        std::cout << "error pase:" << rapidjson::GetParseError_En(doc.GetParseError()) << std::endl;
        return 1;        
    }
```

#### jsonの値の取得
解析が成功したDocumentを使用して、値を取得しています。値の型をIsString関数などのIs関数でチェックして、値の取得にはGetString関数やGetInt関数などそれぞれの型に一致したGet関数で値を取得できます。アレイやオブジェクトを取得するときはrapidjson::Valueに変換してから改めてStringやIntなどの型に応じたGet関数で値を取得しています。
全部の型チェックは手間なので後述するjsonスキーマで型チェックするのがいいかなと思います。


``` cppMain.cpp
    std::cout << "---- string ----" << std::endl;
    if(!doc["pets"].IsString()) {
        std::cout << "pets value is not string" << std::endl;
        return 1;
    }
    const char* a = doc["pets"].GetString();
    std::cout << "pets value:" << a << std::endl;

    std::cout << "---- list ----" << std::endl;
    const rapidjson::Value& c = doc["like"].GetArray();

    for (auto& d : c.GetArray()) {
        const char* e = d.GetString();
        std::cout << "list value:" << e << std::endl;
    }
```

## json形式のファイルの解析
### json形式のファイルの解析の使用例
pet.jsonという名前のファイルを読み込んで、そのjsonのpetsキーの文字列を抜き出し表示しています。

``` cppMain.cpp
#include "rapidjson/document.h"
#include "rapidjson/istreamwrapper.h"
#include <fstream>
#include <iostream>

int main(){

    std::cout << "---- file read ----" << std::endl;
    std::ifstream ifs("pet.json");
    rapidjson::IStreamWrapper isw(ifs);

    std::cout << "---- file parse ----" << std::endl;
    rapidjson::Document doc;
    doc.ParseStream(isw);

    std::cout << "---- string ----" << std::endl;
    const char* a = doc["pets"].GetString();
    std::cout << "pets value:" << a << std::endl;

    return 0;
}
```
### json形式のファイルの解析の説明
#### jsonファイルの読み込み
std::ifstreamクラスを使用してpet.jsonを読み込みます。その後、rapidjson::IStreamWrapperへファイルを読み込んだクラスを入れることでrapidjson用のストリームラッパー形式にします。


``` cppMain.cpp
    std::cout << "---- file read ----" << std::endl;
    std::ifstream ifs("pet.json");
    rapidjson::IStreamWrapper isw(ifs);
```

#### jsonのストリームラッパーの解析
charの解析と同様にrapidjsonのDocumentを宣言します。
charの解析では、Parse関数を使用していましたが、今回はParseStream関数に先ほど作成したrapidjsonのストリームラッパーを読み込ませて解析します。あとは、charと同様にdocを使用して値のチェックや値の取得、変更などができます。


``` cppMain.cpp
    std::cout << "---- file parse ----" << std::endl;
    rapidjson::Document doc;
    doc.ParseStream(isw);
```


## jsonスキーマでのバリデーション
### jsonスキーマでのバリデーションの使用例
jsonスキーマ用ファイルとチェック対象のjsonファイルを読み込んで、チェック対象のjsonがスキーマのチェックが通るかを調べています。


``` cppMain.cpp
#include "rapidjson/document.h"
#include "rapidjson/istreamwrapper.h"
#include "rapidjson/schema.h"
#include "rapidjson/stringbuffer.h"
#include <fstream>
#include <iostream>


int main(){
    std::cout << "---- schema file read ----" << std::endl;
    std::ifstream schema_ifs("schema.json");
    rapidjson::IStreamWrapper schema_isw(schema_ifs);
    std::cout << "---- schema file parse ----" << std::endl;
    rapidjson::Document schema_doc;
    schema_doc.ParseStream(schema_isw);
    rapidjson::SchemaDocument schema(schema_doc);
    rapidjson::SchemaValidator validator(schema);

    std::cout << "---- json file read ----" << std::endl;
    std::ifstream ifs("pet.json");
    rapidjson::IStreamWrapper isw(ifs);
    std::cout << "---- json file parse ----" << std::endl;
    rapidjson::Document doc;
    doc.ParseStream(isw);

    std::cout << "---- json validator ----" << std::endl;
    if (!doc.Accept(validator)) {
        rapidjson::StringBuffer sb;
        validator.GetInvalidDocumentPointer().StringifyUriFragment(sb);
        std::cout << "Invalid schema:" << sb.GetString() << std::endl;
        std::cout << "Invalid keyword:" << validator.GetInvalidSchemaKeyword() << std::endl;
    }
    return 0;
}

```


### jsonスキーマでのバリデーションの説明
#### jsonスキーマファイルの読み込み
上で説明したようにjsonスキーマファイルを読み込んでDocumentに変換します。変換後のDocumentをスキーマ用のDocumentに変換して、Validatorクラスを生成するときに与えてvalidatorを作成しています。

``` cppMain.cpp
    std::cout << "---- schema file read ----" << std::endl;
    std::ifstream schema_ifs("schema.json");
    rapidjson::IStreamWrapper schema_isw(schema_ifs);
    std::cout << "---- schema file parse ----" << std::endl;
    rapidjson::Document schema_doc;
    schema_doc.ParseStream(schema_isw);
    rapidjson::SchemaDocument schema(schema_doc);
    std::cout << "---- schema set validate ----" << std::endl;
    rapidjson::SchemaValidator validator(schema);
```

#### チェック対象ファイルの読み込み
上で説明したようにjsonファイルを読み込んでDocumentに変換します。チェック対象がファイルじゃない(ストリーム)のときも上で説明したようにDocumentに変換します。

#### チェック対象Documentのvalidate
チェック対象のDocumentのAccept関数に上で作成したvalidatorを入れることでチェックがOKかNGかわかります。OKの場合はAcceptがtrueを返却するため、以下のようにif文内にエラーを表示する処理を入れています。

``` cppMain.cpp
    std::cout << "---- json validator ----" << std::endl;
    if (!doc.Accept(validator)) {
        rapidjson::StringBuffer sb;
        validator.GetInvalidDocumentPointer().StringifyUriFragment(sb);
        std::cout << "Invalid schema:" << sb.GetString() << std::endl;
        std::cout << "Invalid keyword:" << validator.GetInvalidSchemaKeyword() << std::endl;
    }
```

バリデーションエラー時の表示

```
Invalid schema:#/age
Invalid keyword:type
```

# おわりに
C++でrapidjsonを使用してjsonを読み込む方法をまとめました。型を意識しなければならないのでpythonに比べれば若干手間ですが、簡単にjsonを読み込めました。さらにjsonスキーマを用いたバリデーションやシリアライズもできるため、pythonでできるjson系の操作は一通りできそうです。