Help us understand the problem. What is going on with this article?

RapidJSONのメモ

More than 3 years have passed since last update.

公式サイトにちゃんとしたドキュメントがあるが、英語がつらいのでメモをとる。

特徴

  • 速度へのこだわり
  • ヘッダだけなので、コピッペするだけで使える
  • Visual Studio, gcc, clang 対応
  • STLなどに依存していない
  • 例外処理なし。善し悪しは別として
  • UTF-8(ASCII)、UTF-16、UTF-32対応

簡単な例

#include "rapidjson/document.h"

static const char* s_json = R"(
{
    "string" : "foo",
    "number" : 123,
    "array" : [
        0,
        1,
        2,
        3
    ],
    "object" : {
        "v0" : "bar",
        "v1" : 456,
        "v2" : 0.123
    }
}
)";

using namespace rapidjson;

void test_example()
{
    Document doc;

    doc.Parse(s_json);
    bool error = doc.HasParseError();
    if(error){
        printf("parse error\n");
        return;
    }

    // string
    {
        const char* v = doc["string"].GetString();
        printf("string = %s\n", v);
    }

    // number
    {
        int v = doc["number"].GetInt();
        printf("number = %d\n", v);
    }

    // array
    {
        const Value& a = doc["array"];
        SizeType num = a.Size();

        for(SizeType i = 0; i < num; i++){
            int v = a[i].GetInt();
            printf("array[%d] = %d\n", i, v);
        }
    }

    // object
    {
        const Value& o = doc["object"];

        // enumerate members in object
        for(Value::ConstMemberIterator itr = o.MemberBegin();
            itr != o.MemberEnd(); itr++)
        {
            const char* name = itr->name.GetString();
            const Value& value = itr->value;
            Type type = value.GetType();

            printf("%s = ", name);
            switch(type){
            case kStringType:
                printf("%s", value.GetString());
                break;

            case kNumberType:
                if(value.IsDouble())
                    printf("%f", value.GetDouble());
                else
                    printf("%d", value.GetInt());
                break;

            default:
                printf("(unknown type)");
                break;
            }
            printf("\n");
        }
    }
}

入出力ストリーム

デフォルトで、メモリとFILE*をサポートしている。

メモリ

読み込みはStringStream、書き込みはStringBufferStreamを使用する。

#include "rapidjson/document.h"
#include "rapidjson/writer.h"

using namespace rapidjson;

void test_stream_memory()
{
    const char* json = R"({"name":"value"})";
    Document doc;

    // read
    StringStream rs(json);

    doc.ParseStream(rs);

    // write
    StringBuffer ws;
    Writer<StringBuffer> writer(ws);

    doc.Accept(writer);

    const char* result = ws.GetString();
    printf("%s\n", result);
}

メモリからの読み込みはよくあることなので、次の簡易版が使える。意味は全く同じ。

    doc.Parse(json);

FILE*

読み込みはFileReadStream、書き込みはFileWriteStreamを使用する。

#include "rapidjson/document.h"
#include "rapidjson/filereadstream.h"
#include "rapidjson/filewritestream.h"
#include "rapidjson/writer.h"

using namespace rapidjson;

void test_stream_file()
{
    FILE* fp;
    char buf[512];
    Document doc;

    // read
    fp = fopen("json.txt", "rb");
    FileReadStream rs(fp, buf, sizeof(buf));

    doc.ParseStream(rs);

    fclose(fp);

    // write
    fp = fopen("tmp.txt", "wb");
    FileWriteStream ws(fp, buf, sizeof(buf));
    Writer<FileWriteStream> writer(ws);

    doc.Accept(writer);

    fclose(fp);
}

DOM

注意点

  • move-semanticsを採用している。代入したりすると所有権が移る
  • コピーが必要な処理ではアロケータを指定する
  • Document、Valueの初期値はNull

Document

DocumentはValueのサブクラスで、DOMのルートにあたる。通常、objectかarray。

パース時のエラーはHasParseError()でわかる。その場合、エラーが発生した位置(オフセット)とエラーコードがとれる。行の情報はない。オマケでエラーコードに対する英文がある。

#include "rapidjson/document.h"
#include "rapidjson/error/en.h"

using namespace rapidjson;

void test_error()
{
    const char* json = R"({"name":value})";// invalid
    Document doc;

    doc.Parse(json);

    bool error = doc.HasParseError();
    if(error){
        size_t offset = doc.GetErrorOffset();
        ParseErrorCode code = doc.GetParseError();
        const char* msg = GetParseError_En(code);

        printf("%d:%d(%s)\n", offset, code, msg);
    }
}

ドキュメントは自身でアロケータを持っており、ドキュメント間でオブジェクトを移動できない。正攻法かどうかわからないが、共有することも可能。

Document doc1;

{
    Document doc2(&doc1.GetAllocator());
    // hogehoge...
}

Value

ValueがどのタイプなのかはIsNumber()などで判定する。実際の値はGetInt()などでとるが、タイプが合わない場合は不正な処理となる。

object

SetObject()かkObjectTypeを指定したコンストラクタで生成。

    Value obj(kObjectType);
    // or obj.SetObject();

    assert(obj.IsObject());

参照。

    const Value& v = obj["key"];

存在しないkeyを参照するのは不正な処理になる。HasMember(key)で存在を確認できるが・・・

    bool exist = doc.HasMember("key");
    if(exist){
        const Value& v = doc["key"];
        // ...
    }

この方法だと2回の参照が発生する。次の方法だと1回で済む。

    Value::ConstMemberIterator itr = doc.FindMember("key");
    if(itr != doc.MemberEnd()){
        const Value& v = itr->value;
        // ...
    }

補足。0.1以下の古くさいバージョンでは、キーとNullの確認は以下のように行っていた。しかし、0.2以降はこのやりかたはできなくなった。(恐らく、キーの検索が2回必要になるためだと思う。0.1のころはFindMemberはprivateになっていて使えない。)

    if(!doc.HasMember("key"))
        return false;// no value
    const Value& val = doc["key"];
    if(val.IsNull())
        return false;// value is null

追加。所有権が移ることに注意。

    obj.AddMember("key", Value(123), doc.GetAllocator());

置き換え。

    obj["key"] = Value("foo");

列挙。

    for(Value::ConstMemberIterator itr = obj.MemberBegin(); itr != obj.MemberEnd(); itr++){
        const Value& n = itr->name;
        const Value& v = itr->value;
        // ...
    }

削除。

    obj.RemoveMember("key");

    Value::MemberIterator itr = doc.FindMember("key");
    if(itr != doc.MemberEnd()){
        obj.RemoveMember(itr);
        //or obj.EraseMember(itr);
    }

    obj.RemoveAllMembers();

削除にはいくつか種類がある。RemoveMember(MemberIterator)はEraseMember()より高速だが、メンバーの順序が維持されない。

array

SetArray()かkArrayTypeで作成。

    Value v(kArrayType);
    // or v.SetArray();

    assert(v.IsArray());

追加削除

    for(int i = 0; i < 3; i++)
        v.PushBack(Value(i), doc.GetAllocator());

    v.PopBack();

参照。置き換え

    assert(v.Size() == 2);
    assert(v[SizeType(0)] == 0);
    assert(v[SizeType(1)] == 1);

    v[SizeType(0)] = 1;

SizeTypeはnullと0を区別できないためにある。

全削除

    v.Clear();

配列用のメソッドは他にもある。

number

SetInt()やSetDouble()、コンストラクタなどで生成。

    Value n1;
    n1.SetInt(1);

    Value n2(2.5);// double
    Value n3(3U);// unsigned
    Value n4(4LL);// 64bit
    Value n5(5LLU);// unsigned 64bit

    assert(n1.GetInt() == 1);
    assert(n2.GetDouble() == 2.5);
    assert(n3.GetUint() == 3);
    assert(n4.GetInt64() == 4);
    assert(n5.GetUint64() == 5);

string

文字列は、ポインタを参照するだけか、複製するかの2種類の作成方法がある。

    Value string1("const-string");// const-string

    Value string2;
    {
        char buf[16];

        sprintf(buf, "%s-%s", "copy", "string");
        string2.SetString(buf, doc.GetAllocator());// copy-string
    }
    const char* a = string2.GetString();
    assert(string2 == "copy-string");

ヌル文字を扱うために、GetStringLength()や長さ付きのコンストラクタがある。

複製

CopyFrom()かAllocatorをとるコンストラクタで可能。

    Value v1;

    Value v2(v1, doc.GetAllocator());
    // or
    v2.CopyFrom(v1, doc.GetAllocator());

入れ替え

Swap()で可能。単に入れ替えるだけなのでDOMツリーの複雑さに依存しない固定速度で行われる。Allocatorの所有者に注意。

    v1.Swap(v2);

In-Situパース

In-Situパースは入力バッファ内で文字列のデコードを行うことで、メモリのアロケーションとコピーのコストを抑える。文字列はコピーされず、単にバッファ内を指すだけになる。当然だが、以下の制限がある。

  • JSON全体がメモリに収まっていること
  • 入力元とDocumentのエンコーディングは同じにすること
  • Documentが不要になるまでバッファを保持していること
  • 文字列が少ないDOMを長期間保持するのは、メモリを無駄にするので、そのような利用には向いていない
#include "rapidjson/document.h"

using namespace rapidjson;

void test_insitu()
{
    FILE* fp;
    char* buf;
    size_t size;

    fp = fopen("json.txt", "rb");
    fseek(fp, 0, SEEK_END);
    size = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    buf = (char*)malloc(size + 1);
    fread(buf, size, 1, fp);
    buf[size] = 0;
    fclose(fp);

    Document doc;
    doc.ParseInsitu(buf);

    // do something...

    free(buf);
}

別のデータ構造へ

DOMの文字列化で使用するValue::Accept()は、実際のところ、ハンドラー(Writer)へSAXイベント送っている。ValueがSAXイベントを送り、WriterがSAXイベントを処理している。

カスタムのハンドラーを使うことで、DOMを別のフォーマットへ変更できる。例えばDOMからXMLなど。

ハンドラーの詳細はSAXの節で。

アロケータ

アプリケーションによっては極力メモリのアロケーションを抑えたいかもしれない。

DocumentのデフォルトのアロケータであるMemoryPoolAllocatorは、ある程度の大きさ(コードを見ると64k単位)をまとめてアロケートすることでアロケーションのコストを抑えている。が、MemoryPoolAllocatorは事前に静的に確保された領域を使うこともできる。

#include "rapidjson/document.h"

using namespace rapidjson;

void test_allocator()
{
    const char* json = R"({"name":"value"})";
    char buffer[1024];

    MemoryPoolAllocator<> allocator(buffer, sizeof(buffer));

    Document doc(&allocator);

    doc.Parse(json);
}

アロケーションのサイズがバッファサイズ(この例だと1024)を超えない限り、通常のアロケータ(CrtAllocator=malloc,free)は使用されない。

エンコーディング

デフォルトでUTF-8だが他も扱える。

通常、メモリ上(Documentなど)のエンコーディングには、UTF-8、UTF-16、UTF-32、ストリームには、UTF-8、UTF-16LE、UTF-16BE、UTF-32LE、UTF-32BE、ASCII(7 bit)を使う。ASCIIはメモリ上のエンコーディングには使えない。ASCIIの場合、127を超える値はエスケープされた文字列(\uXXXX)として処理される。

DocumentやValueは、GenericDocument, GenericValueのEncodingをUTF8にし、typedefしたものであり、他のエンコーディングを使いたい場合は同じようにする。

#include "rapidjson/document.h"
#include "rapidjson/writer.h"

using namespace rapidjson;

typedef GenericDocument< UTF16<> > Document_UTF16;
typedef GenericValue< UTF16<> > Value_UTF16;
typedef GenericStringStream< UTF16<> > StringStream_UTF16;
typedef GenericStringBuffer< UTF16<> > StringBuffer_UTF16;

void test_encoding_memory()
{
    const wchar_t* json = LR"({"name":"value"})";
    Document_UTF16 doc;

    StringStream_UTF16 rs(json);

    doc.ParseStream(rs);

    doc[L"name"] = Value_UTF16(L"bar");

    StringBuffer_UTF16 s;
    Writer< StringBuffer_UTF16, UTF16<>, UTF16<> > writer(s);

    doc.Accept(writer);

    const wchar_t* wstr = s.GetString();
}

FileReadStream、FileWriteStreamはただのバイトストリームであり、エンコーディングの概念はない。これらのストリームでUTF8以外を扱うにはEncodedInputStream、EncodedOutputStreamでこれらをラップする必要がある。

EncodedOutputStreamはBOMを付けるかどうか制御可能。

#include "rapidjson/document.h"
#include "rapidjson/filereadstream.h"
#include "rapidjson/filewritestream.h"
#include "rapidjson/writer.h"
#include "rapidjson/encodedstream.h"

using namespace rapidjson;

typedef GenericDocument< UTF16<> > Document_UTF16;
typedef GenericValue< UTF16<> > Value_UTF16;

typedef EncodedInputStream<UTF16LE<>, FileReadStream> EncodedInputStream_UTF16;
typedef EncodedOutputStream<UTF16LE<>, FileWriteStream> EncodedOutputStream_UTF16;

void test_encoding_file()
{
    FILE* fp;
    char buf[256];
    Document_UTF16 doc;

    fp = fopen("json-utf16le.txt", "rb");
    FileReadStream rs(fp, buf, sizeof(buf));
    EncodedInputStream_UTF16 is(rs);

    doc.ParseStream(is);

    fclose(fp);

    doc[L"name"] = Value_UTF16(L"foo");

    fp = fopen("tmp.txt", "wb");
    FileWriteStream ws(fp, buf, sizeof(buf));
    EncodedOutputStream_UTF16 os(ws);// with BOM
    Writer< EncodedOutputStream_UTF16, UTF16<>, UTF16LE<> > writer(os);

    doc.Accept(writer);

    fclose(fp);
}

前述の方法はエンコーディングが固定されている。実行時にエンコーディングを決定したい場合は、AutoUTFInputStream、AutoUTFOutputStreamを使用する。AutoUTFInputStreamはエンコーディングを自動判定する。また、AutoUTFOutputStreamはエンコーディングを指定できる。

#include "rapidjson/document.h"
#include "rapidjson/filereadstream.h"
#include "rapidjson/filewritestream.h"
#include "rapidjson/writer.h"
#include "rapidjson/encodedstream.h"

using namespace rapidjson;

void test_encoding_auto()
{
    FILE* fp;
    char buf[256];
    Document doc;

    // read
    fp = fopen("json-utf16le.txt", "rb");
    FileReadStream rs(fp, buf, sizeof(buf));
    AutoUTFInputStream<unsigned, FileReadStream> is(rs);

    doc.ParseStream<kParseDefaultFlags, AutoUTF<unsigned> >(is);

    fclose(fp);

    // do something...
    UTFType utf_type = is.GetType();
    assert(utf_type == kUTF16LE);
    bool has_bom = is.HasBOM();
    assert(has_bom == true);

    doc["name"] = Value("hoge");

    // write
    typedef AutoUTFOutputStream<unsigned, FileWriteStream> OutputStream;

    fp = fopen("tmp.txt", "wb");
    FileWriteStream ws(fp, buf, sizeof(buf));
    bool put_bom = true;
    OutputStream os(ws, kUTF16LE, put_bom);
    Writer<OutputStream, Document::EncodingType, AutoUTF<unsigned> > writer(os);

    doc.Accept(writer);

    fclose(fp);
}

EncodedInputStreamなどを使うよりも簡単だが、少しだけ変換するオーバーヘッドがある。

オマケ

RapidJSONはJSONを扱うための物だが、エンコーディングを変換するためだけにも使える。

    StringStream source(".... utf8 string ...");
    GenericStringBuffer< UTF16<> > target;

    while(source.Peek() != '\0'){
        bool error = Transcoder< UTF8<>, UTF16<> >::Transcode(source, target);
        if(error){
            break;
        }
    }

    const wchar_t* wstr = target.GetString();

SAX

ちょっとしたJSONを処理したり、JSONから、DOMではない、別のデータ構造を作る場合はSAXのほうが効率的。

Reader

ReaderはJSONをパースしながらハンドラーへイベントを送る。ハンドラーは次のコードにあるHandlerクラスと同じメンバー関数を持たなければならない。

#include "rapidjson/reader.h"

using namespace rapidjson;

class Handler {
public:
    bool Null()
    {
        return true;
    }

    bool Bool(bool b)
    {
        return true;
    }

    bool Int(int i)
    {
        return true;
    }

    bool Uint(unsigned i)
    {
        return true;
    }

    bool Int64(int64_t i)
    {
        return true;
    }

    bool Uint64(uint64_t i)
    {
        return true;
    }

    bool Double(double d)
    {
        return true;
    }

    bool String(const char* str, SizeType length, bool copy)
    {
        return true;
    }

    bool StartObject()
    {
        return true;
    }

    bool Key(const char* str, SizeType length, bool copy)
    {
        return true;
    }

    bool EndObject(SizeType memberCount)
    {
        return true;
    }

    bool StartArray()
    {
        return true;
    }

    bool EndArray(SizeType elementCount)
    {
        return true;
    }
};

void test_sax_reader()
{
    const char* json = R"({"key":"value"})";

    Handler handler;
    Reader reader;

    reader.Parse(StringStream(json), handler);

    if(reader.HasParseError()){
        ParseErrorCode error_code = reader.GetParseErrorCode();
    }
}

この例では、StartObject()、Key()、String()、EndObject()の順に呼ばれる。

通常、各関数はtrueを返すが、処理を停止したい場合はfalseを返す。この場合、エラーコードにkParseErrorTerminationが設定される。

ほとんどの関数は名前から機能が予測できる。String()とKey()は引数が多いが、以下の意味を持つ。

  • str 文字列へのポインタ。型はターゲットにあわせる必要がある
  • length 終端文字を含まない文字数。これはヌル文字を扱うため
  • copy trueの場合は文字列をコピーする必要がある。これがfalseになるのはIn-Situパースの時のみ

Readerは、DocumentやValueと同じように、GenericReaderをUTF8でtypedefしたものなので、他のエンコーディングを扱いたいときには同じようにする。

エラーの処理方法はDocumentと同じ。というか、DocumentがReaderと同じ。

Writer

Writerは、Readerとは逆に、イベントをJSONに変換する。WriterのイベントはReaderで説明したハンドラーと全く同じ。オマケでString()とKey()の簡易版がある。

#include "rapidjson/writer.h"
#include "rapidjson/prettywriter.h"
#include <stdio.h>

using namespace rapidjson;

void test_sax_writer()
{
    StringBuffer s;
#if 1
    Writer<StringBuffer> writer(s);
#else
    PrettyWriter<StringBuffer> writer(s);
    writer.SetIndent('\t', 1);
#endif

    writer.StartObject();

    writer.Key("key");
    writer.String("value");

    writer.Key("object");
    writer.StartObject();
        writer.Key("key");
        writer.Int(123);
    writer.EndObject();

    writer.EndObject();

    const char* result = s.GetString();
    printf("%s\n", result);
}

不正な順番で呼び出してはならない。デバッグバージョンではassertする。EndObject()とEndArray()の引数は不要。

出力が完了、StartObject()してEndObject()、した後は、そのままでは、再利用できない。再利用するためには、新しいストリームでリセット(Writer::Reset(OutputStream& os))する必要がある。

Writerは空白のない詰まったJSONを出力する。これは保存や送信に向いているが、人が読みやすいJSONがほしい場合はPrettyWriterが使える。

PrettyWriter

PrettyWriterはインデントや改行された読みやすいJSONを出力する。それ以外はWriterと同じ。
デフォルトでは4つのスペースでインデントされるが、SetIndent()で変えることができる。

オマケ(フィルターとして使う)

WriterはReaderが必要とするハンドラーの関数をすべて備えている。ようは、WriterはReaderのハンドラーとすることができる。

Writerは隙間のない詰まったJSONを出力する。また、PrettyWriterは改行された読みやすいJSONを出力する。このため、単にJSONをReaderとWriterに通すだけで再フォーマットすることができる。

#include "rapidjson/reader.h"
#include "rapidjson/prettywriter.h"
#include <stdio.h>

using namespace rapidjson;

void test_sax_reformat()
{
    const char* json = R"({       "key"  :    "value"     })";

    Reader reader;
    StringBuffer buf;
    PrettyWriter<StringBuffer> writer(buf);

    bool succeeded = reader.Parse(StringStream(json), writer);

    const char* result = buf.GetString();
    printf("%s\n", result);
}

もちろん、Reader -> カスタムハンドラー -> Writerとすることで、何らかの、変換やチェックといった、フィルター処理も可能。

カスタムストリーム

メモリとファイル(FILE*)以外のストリームが必要なら自作できる。

ストリームは以下のメンバ関数を実装しなければならない。

読み込み:
Peek() 現在位置の文字を返し、位置は変えない。
Take() 現在位置の文字を返し、位置を進める。
Tell() 現在位置を返す。

書き込み:
Put() 文字を書き込む。
Flush() バッファのフラッシュ。

以下の関数はIn-Situパース用なので実装は不要だが、コンパイルエラーを回避するため関数だけは用意しておく。
PutBegin()
PutEnd()

#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include <fstream>

using namespace rapidjson;

class CustomStream {
    std::iostream& m_s;

public:
    CustomStream(std::iostream& s)
        : m_s(s)
    {
    }

public:
    typedef char Ch;

    Ch Peek() const
    {
        if(m_s.eof())
            return '\0';
        return m_s.peek();
    }

    Ch Take()
    {
        if(m_s.eof())
            return '\0';
        return m_s.get();
    }

    size_t Tell()
    {
        return m_s.tellg();
    }

    Ch* PutBegin()
    {
        return '\0';// dummy
    }

    void Put(Ch c)
    {
        m_s.put(c);
    }

    void Flush()
    {
        m_s.flush();
    }

    size_t PutEnd(Ch* begin)
    {
        return '\0';// dummy
    }
};

void test_stream_custom()
{
    Document doc;

    // read
    std::fstream is("json.txt", std::ios::in);
    CustomStream rs(is);

    doc.ParseStream(rs);

    is.close();

    // do something...
    doc["name"] = Value("foo");

    // write
    std::fstream os("tmp.txt", std::ios::out);
    CustomStream ws(os);
    Writer<CustomStream> writer(ws);

    doc.Accept(writer);

    os.close();
}
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away