このチュートリアルでは、Document Object Model(DOM)APIの基本を紹介します。

使い方の概要で示したように、JSONをDOMへ解析し、DOMへ簡単に問い合わせや変更を行い、最後にJSONへ戻せます。

ValueとDocument

各JSON値は、Valueという型に保存します。DOMを表すDocumentは、DOMツリーのルートとなるValueを保持します。RapidJSONの公開型と公開関数はすべて、rapidjson名前空間に定義されています。

値の問い合わせ

この節では、example/tutorial/tutorial.cppの抜粋を使います。

C文字列(const char* json)にJSONを保存したとします。

{
"hello": "world",
"t": true ,
"f": false,
"n": null,
"i": 123,
"pi": 3.1416,
"a": [1, 2, 3, 4]
}

これをDocumentへ解析します。

using namespace rapidjson;
// ...
Document document;
document.Parse(json);
GenericDocument & Parse(const typename SourceEncoding::Ch *str)
読み取り専用文字列からJSONテキストを解析します(エンコーディング変換あり)。
Definition document.h:2221
RapidJSONの主な名前空間。
Definition rapidjson.h:374
GenericDocument< UTF8<> > Document
UTF8エンコーディングのGenericDocument。
Definition document.h:2402

JSONは、DOMツリーとしてdocumentへ解析されました。

チュートリアルのDOM

RFC 7159への更新以降、準拠するJSON文書のルートには、どのJSON値でも使えます。それ以前のRFC 4627では、ルート値にはオブジェクトか配列しか使えませんでした。この例のルートはオブジェクトです。

assert(document.IsObject());

ルートオブジェクトに"hello"メンバーがあるか問い合わせてみます。Valueは異なる型の値を保持できるため、型を確認し、適切なAPIで値を取得する必要がある場合があります。この例では、"hello"メンバーはJSON文字列に対応しています。

assert(document.HasMember("hello"));
assert(document["hello"].IsString());
printf("hello = %s\n", document["hello"].GetString());
bool HasMember(const Ch *name) const
オブジェクトにメンバーがあるかどうかを確認します。
Definition document.h:1094
world

JSONのtrue/false値は、boolで表します。

assert(document["t"].IsBool());
printf("t = %s\n", document["t"].GetBool() ? "true" : "false");
true

JSONのnullは、IsNull()で問い合わせられます。

printf("n = %s\n", document["n"].IsNull() ? "null" : "?");
null

JSONの数値型は、すべての数値を表します。ただし、C++で操作するには、より具体的な型が必要です。

assert(document["i"].IsNumber());
// In this case, IsUint()/IsInt64()/IsUInt64() also return true.
assert(document["i"].IsInt());
printf("i = %d\n", document["i"].GetInt());
// Alternative (int)document["i"]
assert(document["pi"].IsNumber());
assert(document["pi"].IsDouble());
printf("pi = %g\n", document["pi"].GetDouble());
i = 123
pi = 3.1416

JSON配列は、複数の要素を保持します。

// Using a reference for consecutive access is handy and faster.
const Value& a = document["a"];
assert(a.IsArray());
for (SizeType i = 0; i < a.Size(); i++) // Uses SizeType instead of size_t
printf("a[%d] = %d\n", i, a[i].GetInt());
SizeType Size() const
配列の要素数を取得します。
Definition document.h:1471
unsigned SizeType
大きさを表す型(文字列の長さ、配列の大きさなど)。
Definition rapidjson.h:380
GenericValue< UTF8<> > Value
UTF8エンコーディングのGenericValue。
Definition document.h:2010
a[0] = 1
a[1] = 2
a[2] = 3
a[3] = 4

RapidJSONは、JSONの型の間で値を自動変換しません。たとえば、値が文字列なら、GetInt()の呼び出しは不正です。デバッグモードではアサーションに失敗し、リリースモードでは未定義の動作になります。

以下では、個々の型に問い合わせる方法を詳しく説明します。

配列の問い合わせ

既定では、SizeTypeはunsignedのtypedefです。大半のシステムでは、配列が保存できる要素数は最大2^32-1に制限されます。

a[0]、a[1]、a[2]など、整数リテラルで配列の要素へアクセスできます。

配列はstd::vectorと同様、インデックスの代わりにイテレーターを使って、すべての要素へアクセスすることもできます。

for (Value::ConstValueIterator itr = a.Begin(); itr != a.End(); ++itr)
printf("%d ", itr->GetInt());
const GenericValue * ConstValueIterator
Definition document.h:551
ValueIterator Begin()
要素のイテレーター。
Definition document.h:1505
ValueIterator End()
末尾の次を指す要素イテレーター。
Definition document.h:1508

他にも、よく使う問い合わせ関数があります。

  • SizeType Capacity() const
  • bool Empty() const

範囲ベースのforループ(v1.1.0で追加)

C++11が有効なら、範囲ベースのforループで配列内のすべての要素へアクセスできます。

for (auto& v : a.GetArray())
printf("%d ", v.GetInt());

オブジェクトの問い合わせ

配列と同様、イテレーターですべてのオブジェクトメンバーへアクセスできます。

static const char* kTypeNames[] =
{ "Null", "False", "True", "Object", "Array", "String", "Number" };
itr != document.MemberEnd(); ++itr)
{
printf("Type of member %s is %s\n",
itr->name.GetString(), kTypeNames[itr->value.GetType()]);
}
ConstMemberIterator MemberEnd() const
末尾の次を指すconstメンバーイテレーター。
Definition document.h:1078
GenericMemberIterator< true, UTF8<>, MemoryPoolAllocator<> >::Iterator ConstMemberIterator
Definition document.h:549
ConstMemberIterator MemberBegin() const
constメンバーイテレーター。
Definition document.h:1075
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

operator[](const char*)でメンバーが見つからないと、アサーションに失敗することに注意してください。

メンバーが存在するか不明な場合は、operator[](const char*)の前にHasMember()を呼ぶ必要があります。ただし、これでは検索を2回行います。FindMember()を呼べば、メンバーの存在確認と値の取得を一度に行えるため、より良い方法です。

Value::ConstMemberIterator itr = document.FindMember("hello");
if (itr != document.MemberEnd())
printf("%s\n", itr->value.GetString());
MemberIterator FindMember(const Ch *name)
名前でメンバーを検索します。
Definition document.h:1132

範囲ベースのforループ(v1.1.0で追加)

C++11が有効なら、範囲ベースのforループでオブジェクト内のすべてのメンバーへアクセスできます。

for (auto& m : document.GetObject())
printf("Type of member %s is %s\n",
m.name.GetString(), kTypeNames[m.value.GetType()]);

数値の問い合わせ

JSONには、Numberという単一の数値型があります。Numberは整数か実数を表せます。RFC 4627では、Numberの範囲はパーサーが定めるとしています。

C++には複数の整数型と浮動小数点型があるため、DOMはできるだけ広い範囲と良い性能で、これらを扱おうとします。

Numberを解析すると、DOMでは以下のいずれかの型で保存します。

型説明
unsigned32ビットの符号なし整数
int32ビットの符号付き整数
uint64_t64ビットの符号なし整数
int64_t64ビットの符号付き整数
double64ビットの倍精度浮動小数点数

数値へ問い合わせるときは、その数値を対象の型として取得できるか確認できます。

確認取得
bool IsNumber()該当なし
bool IsUint()unsigned GetUint()
bool IsInt()int GetInt()
bool IsUint64()uint64_t GetUint64()
bool IsInt64()int64_t GetInt64()
bool IsDouble()double GetDouble()

整数値は、変換せずに複数の方法で取得できる場合があります。たとえば、123を保持する値xでは、x.IsInt() == x.IsUint() == x.IsInt64() == x.IsUint64() == trueとなります。一方、-3000000000を保持する値yでは、x.IsInt64() == trueだけが成立します。

数値を取得するとき、GetDouble()は内部の整数表現をdoubleへ変換します。intとunsignedはdoubleへ安全に変換できますが、int64_tとuint64_tでは精度を失う場合があります(doubleの仮数部は52ビットしかないためです)。

文字列の問い合わせ

Valueクラスには、GetString()に加えてGetStringLength()もあります。その理由を説明します。

RFC 4627によると、JSON文字列にはUnicode文字U+0000を含めることができ、"\u0000"としてエスケープしなければなりません。問題は、C/C++ではヌル終端文字列をよく使い、\0'を終端記号として扱うことです。

RFC 4627に準拠するため、RapidJSONはU+0000を含む文字列に対応しています。これを扱う必要がある場合は、GetStringLength()APIで正しい文字列長を取得できます。

たとえば、以下のJSONをDocument dへ解析した場合です。

{ "s" : "a\u0000b" }

値"a\u0000b"の正しい長さは3ですが、strlen()は1を返します。

バッファーの確保のためにstrlen()を呼ぶことが多いため、GetStringLength()は性能の改善にも役立ちます。

さらに、std::stringには次のコンストラクターもあります。

string(const char* s, size_t count);

これは、引数として文字列の長さを受け取ります。文字列内のヌル文字の保存にも対応し、性能の向上も期待できます。

値の比較

値の比較には、==と!=を使えます。2つの値が等しいのは、型と内容がともに同じ場合に限ります。プリミティブ型とも比較できます。例を示します。

if (document["hello"] == document["n"]) /*...*/; // Compare values
if (document["hello"] == "world") /*...*/; // Compare value with literal string
if (document["i"] != 123) /*...*/; // Compare with integers
if (document["pi"] != 3.14) /*...*/; // Compare with double.

配列やオブジェクトは、要素やメンバーを順番に比較します。部分ツリー全体が等しい場合に限り、等しいと判定されます。

現時点では、オブジェクトに名前が重複するメンバーがあると、どのオブジェクトとの等価比較も常にfalseとなることに注意してください。

値の作成・変更

値を作る方法はいくつかあります。DOMツリーを作成・変更した後は、Writerで再びJSONとして保存できます。

値の型の変更

既定のコンストラクターでValueやDocumentを作ると、型はNullになります。型を変更するには、SetXXX()か代入演算子を使います。たとえば、次のようになります。

Document d; // Null
Value v; // Null
v.SetInt(10);
v = 10; // Shortcut, same as above
GenericValue & SetObject()
この値を空のオブジェクトにします。
Definition document.h:1013

オーバーロードされたコンストラクター

複数の型について、オーバーロードされたコンストラクターもあります。

Value b(true); // calls Value(bool)
Value i(-123); // calls Value(int)
Value u(123u); // calls Value(unsigned)
Value d(1.5); // calls Value(double)

空のオブジェクトや配列を作るには、既定のコンストラクターの後にSetObject()/SetArray()を使うか、Value(Type)で一度に作れます。

@ kArrayType
配列
Definition rapidjson.h:608
@ kObjectType
オブジェクト
Definition rapidjson.h:607

ムーブセマンティクス

RapidJSONの設計で特徴的な決定は、値の代入で、代入元を代入先へコピーしないことです。代わりに、代入元の値を代入先へ移動します。たとえば、次のようになります。

Value a(123);
Value b(456);
b = a; // a becomes a Null value, b becomes number 123.
ムーブセマンティクスによる代入。

なぜでしょうか。このセマンティクスの利点は何でしょうか。

簡単に言えば、性能のためです。大きさが固定のJSON型(Number、True、False、Null)は、高速かつ簡単にコピーできます。しかし、可変長のJSON型(String、Array、Object)では、コピーに大きなオーバーヘッドが生じます。このオーバーヘッドは見落とされがちです。特に、一時オブジェクトを作り、別の変数へコピーし、破棄する場合に発生します。

たとえば、通常のコピーセマンティクスを使った場合です。

{
Value contacts(kArrayType);
// adding elements to contacts array.
// ...
o.AddMember("contacts", contacts, d.GetAllocator()); // deep clone contacts (may be with lots of allocations)
// destruct contacts.
}
Allocator & GetAllocator()
この文書のアロケーターを取得します。
Definition document.h:2308
コピーセマンティクスでは、多くのコピー操作が発生します。

オブジェクトoは、contactsと同じ大きさのバッファーを確保し、ディープコピーを作る必要があります。その後、最後にcontactsが破棄されます。これにより、不要なメモリー確保・解放やコピーが大量に発生します。

参照カウントやガベージコレクション(GC)など、実際にこれらのデータをコピーせずに済ませる方法もあります。

RapidJSONを単純かつ高速にするため、代入にはムーブセマンティクスを採用しました。代入時に所有権を移すstd::auto_ptrに似ています。ムーブはずっと高速で単純です。元の値を破棄し、memcpy()で代入元を代入先へコピーし、最後に代入元をNull型にするだけです。

ムーブセマンティクスを使うと、上の例は次のようになります。

{
Value contacts(kArrayType);
// adding elements to contacts array.
o.AddMember("contacts", contacts, d.GetAllocator()); // just memcpy() of contacts itself to the value of new member (16 bytes)
// contacts became Null here. Its destruction is trivial.
}
ムーブセマンティクスではコピーが発生しません。

これは、C++11ではムーブ代入演算子と呼ばれます。RapidJSONはC++03に対応しているため、代入演算子と、AddMember()やPushBack()など他のすべての変更用関数にムーブセマンティクスを採用しています。

ムーブセマンティクスと一時値

PushBack()やAddMember()などの「移動する」関数へ渡す前に、その場でValueを作ると便利な場合があります。一時オブジェクトは適切なValueの参照へ変換できないため、補助関数Move()が用意されています。

Document::AllocatorType& allocator = document.GetAllocator();
// a.PushBack(Value(42), allocator); // will not compile
a.PushBack(Value().SetInt(42), allocator); // fluent API
a.PushBack(Value(42).Move(), allocator); // same as above
MemoryPoolAllocator<> AllocatorType
Definition document.h:2028
GenericValue & PushBack(GenericValue &value, Allocator &allocator)
GenericValueを配列の末尾へ追加します。
Definition document.h:1541

文字列の作成

RapidJSONは、文字列の保存に2つの方法を提供します。

  1. copy-string:バッファーを確保して、元のデータをコピーします。
  2. const-string:文字列へのポインターだけを保存します。

copy-stringはデータのコピーを所有するため、常に安全です。const-stringは、文字列リテラルの保存と、Documentの節で説明するin-situ解析に使えます。

メモリー確保を独自に扱えるよう、RapidJSONでは、メモリー確保が必要になり得る操作には、アロケーターのインスタンスを渡す必要があります。この設計は、Valueごとにアロケーター(またはDocument)へのポインターを保存しないために必要です。

そのため、copy-stringを代入するときは、アロケーター付きの、オーバーロードされたSetString()を呼びます。

Document document;
Value author;
char buffer[10];
int len = sprintf(buffer, "%s %s", "Milo", "Yip"); // dynamically created string.
author.SetString(buffer, len, document.GetAllocator());
memset(buffer, 0, sizeof(buffer));
// author.GetString() still contains "Milo Yip" after buffer is destroyed
GenericValue & SetString(const Ch *s, SizeType length)
元の文字列をコピーせず、この値を文字列として設定します。
Definition document.h:1696

この例では、Documentのインスタンスからアロケーターを取得します。これは、RapidJSONでよく使う書き方ですが、別のアロケーターのインスタンスを使うこともできます。

上のSetString()には長さも必要です。そのため、文字列内のヌル文字も扱えます。長さ引数のない別のSetString()のオーバーロードもあります。そちらは入力がヌル終端されていると仮定し、strlen()のような関数で長さを取得します。

最後に、文字列リテラルや寿命が保証される文字列には、アロケーター引数のないconst-string版のSetString()を使えます。文字列リテラル(または定数の文字配列)なら、引数としてそのまま渡すだけで安全かつ効率的です。

s.SetString("rapidjson"); // can contain null character, length derived at compile time
s = "rapidjson"; // shortcut, same as above

文字ポインターをコピーせずに使うには、RapidJSONでは安全なものとして明示する必要があります。これは、StringRef関数で行えます。

const char * cstr = getenv("USER");
size_t cstr_len = ...; // in case length is available
// s.SetString(cstr); // will not compile
s.SetString(StringRef(cstr)); // ok, assume safe lifetime, null-terminated
s = StringRef(cstr); // shortcut, same as above
s.SetString(StringRef(cstr,cstr_len)); // faster, can contain null character
s = StringRef(cstr,cstr_len); // shortcut, same as above
GenericStringRef< CharType > StringRef(const CharType *str)
文字ポインターを定数文字列として明示します。
Definition document.h:346

配列の変更

配列型のValueは、std::vectorに似たAPIを提供します。

  • Clear()
  • Reserve(SizeType, Allocator&)
  • Value& PushBack(Value&, Allocator&)
  • template <typename T> GenericValue& PushBack(T, Allocator&)
  • Value& PopBack()
  • ValueIterator Erase(ConstValueIterator pos)
  • ValueIterator Erase(ConstValueIterator first, ConstValueIterator last)

Reserve(...)とPushBack(...)は、配列要素用のメモリーを確保する場合があるため、アロケーターが必要です。

PushBack()の例を示します。

Document::AllocatorType& allocator = document.GetAllocator();
for (int i = 5; i <= 10; i++)
a.PushBack(i, allocator); // allocator is needed for potential realloc().
// Fluent interface
a.PushBack("Lua", allocator).PushBack("Mio", allocator);

STLと異なり、PushBack()/PopBack()は配列自身の参照を返します。これを流暢なインターフェースと呼びます。

配列に、定数ではない文字列や寿命が十分でない文字列(文字列の作成を参照)を追加するには、copy-string APIで文字列のValueを作る必要があります。中間変数を用意せずに、その場で一時値を使うこともできます。

// in-place Value parameter
contact.PushBack(Value("copy", document.GetAllocator()).Move(), // copy string
document.GetAllocator());
// explicit parameters
Value val("key", document.GetAllocator()); // copy string
contact.PushBack(val, document.GetAllocator());

オブジェクトの変更

オブジェクトは、キーと値の組(メンバー)の集合です。キーはすべて文字列値でなければなりません。変更するには、メンバーを追加・削除します。以下は、メンバーの追加用APIです。

  • Value& AddMember(Value&, Value&, Allocator& allocator)
  • Value& AddMember(StringRefType, Value&, Allocator&)
  • template <typename T> Value& AddMember(StringRefType, T value, Allocator&)

例を示します。

Value contact(kObject);
contact.AddMember("name", "Milo", document.GetAllocator());
contact.AddMember("married", true, document.GetAllocator());

StringRefTypeによる名前の引数は、文字列値用のSetString関数のインターフェースに似ています。JSONオブジェクトでは定数のキー名をよく使うため、これらのオーバーロードによってname文字列のコピーを避けられます。

定数ではない文字列や寿命が十分でない文字列(文字列の作成を参照)から名前を作るには、copy-string APIで文字列のValueを作る必要があります。中間変数を用意せずに、その場で一時値を使うこともできます。

// in-place Value parameter
contact.AddMember(Value("copy", document.GetAllocator()).Move(), // copy string
Value().Move(), // null value
document.GetAllocator());
// explicit parameters
Value key("key", document.GetAllocator()); // copy string name
Value val(42); // some value
contact.AddMember(key, val, document.GetAllocator());

メンバーの削除には、いくつかの方法があります。

  • bool RemoveMember(const Ch* name):名前を検索してメンバーを削除します(時間計算量は線形)。
  • bool RemoveMember(const Value& name):上と同じですが、nameはValueです。
  • MemberIterator RemoveMember(MemberIterator):イテレーターでメンバーを削除します(時間計算量は定数)。
  • MemberIterator EraseMember(MemberIterator):上に似ていますが、メンバーの順序を保持します(時間計算量は線形)。
  • MemberIterator EraseMember(MemberIterator first, MemberIterator last):範囲内のメンバーを削除し、順序を保持します(時間計算量は線形)。

MemberIterator RemoveMember(MemberIterator)は、「move-last」という方法で定数時間を実現します。基本的には、イテレーター位置のメンバーを破棄し、最後の要素をその位置へ移動します。そのため、残ったメンバーの順序が変わります。

値のディープコピー

DOMツリーを実際にコピーする必要がある場合は、アロケーター付きのコンストラクターとCopyFrom()という2つのAPIでディープコピーできます。

Value v1("foo");
// Value v2(v1); // not allowed
Value v2(v1, a); // make a copy
assert(v1.IsString()); // v1 untouched
d.SetArray().PushBack(v1, a).PushBack(v2, a);
assert(v1.IsNull() && v2.IsNull()); // both moved to d
v2.CopyFrom(d, a); // copy whole document to v2
assert(d.IsArray() && d.Size() == 2); // d untouched
v1.SetObject().AddMember("array", v2, a);
d.PushBack(v1, a);
GenericValue & SetArray()
この値を空の配列にします。
Definition document.h:1468

値の交換

Swap()も用意されています。

Value a(123);
Value b("Hello");
a.Swap(b);
assert(a.IsString());
assert(b.IsInt());

2つのDOMツリーは、ツリーの複雑さにかかわらず、高速(定数時間)に交換できます。

次に読むもの

このチュートリアルでは、DOMツリーへの問い合わせと操作の基本を示しました。RapidJSONには、他にも重要な概念があります。

  1. ストリームはJSONを読み書きする経路で、メモリー内の文字列やファイルストリームなどがあります。独自のストリームも作れます。
  2. エンコーディングは、ストリームやメモリーで使う文字エンコーディングを定義します。RapidJSONは、Unicodeの変換と妥当性検証も内部で処理します。
  3. DOMの基本は、このチュートリアルで説明しました。in situ解析やその他の解析オプション、応用的な使い方などを学べます。
  4. SAXは、RapidJSONの解析・生成機能の基盤です。Reader/Writerでさらに高速なアプリケーションを実装する方法を学べます。また、PrettyWriterによるJSONの整形も試してください。
  5. 性能では、開発元と第三者によるベンチマークを紹介します。
  6. 内部設計では、RapidJSON内部の設計やテクニックを説明します。

FAQ、API文書、サンプル、単体テストも参照できます。