「SAX」という用語は、Simple API for XMLに由来します。JSONの解析・生成にも、この用語を借用しています。

RapidJSONでは、Reader(GenericReader<...>のtypedef)がJSONのSAX形式パーサーで、Writer(GenericWriter<...>のtypedef)がJSONのSAX形式生成器です。

Reader

Readerは、ストリームからJSONを解析します。ストリームの文字を読みながらJSONの構文に従って解析し、ハンドラーへイベントを発行します。

たとえば、次のJSONを考えます。

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

Readerは、このJSONを解析しながら、以下のイベントをハンドラーへ順番に発行します。

StartObject()
Key("hello", 5, true)
String("world", 5, true)
Key("t", 1, true)
Bool(true)
Key("f", 1, true)
Bool(false)
Key("n", 1, true)
Null()
Key("i")
UInt(123)
Key("pi")
Double(3.1416)
Key("a")
StartArray()
Uint(1)
Uint(2)
Uint(3)
Uint(4)
EndArray(4)
EndObject(7)

これらのイベントは、JSONと簡単に対応付けられますが、一部の引数には説明が必要です。上とまったく同じ出力を生成するsimplereaderの例を見てみます。

#include <iostream>
using namespace rapidjson;
using namespace std;
struct MyHandler : public BaseReaderHandler<UTF8<>, MyHandler> {
bool Null() { cout << "Null()" << endl; return true; }
bool Bool(bool b) { cout << "Bool(" << boolalpha << b << ")" << endl; return true; }
bool Int(int i) { cout << "Int(" << i << ")" << endl; return true; }
bool Uint(unsigned u) { cout << "Uint(" << u << ")" << endl; return true; }
bool Int64(int64_t i) { cout << "Int64(" << i << ")" << endl; return true; }
bool Uint64(uint64_t u) { cout << "Uint64(" << u << ")" << endl; return true; }
bool Double(double d) { cout << "Double(" << d << ")" << endl; return true; }
bool String(const char* str, SizeType length, bool copy) {
cout << "String(" << str << ", " << length << ", " << boolalpha << copy << ")" << endl;
return true;
}
bool StartObject() { cout << "StartObject()" << endl; return true; }
bool Key(const char* str, SizeType length, bool copy) {
cout << "Key(" << str << ", " << length << ", " << boolalpha << copy << ")" << endl;
return true;
}
bool EndObject(SizeType memberCount) { cout << "EndObject(" << memberCount << ")" << endl; return true; }
bool StartArray() { cout << "StartArray()" << endl; return true; }
bool EndArray(SizeType elementCount) { cout << "EndArray(" << elementCount << ")" << endl; return true; }
};
void main() {
const char json[] = " { \"hello\" : \"world\", \"t\" : true , \"f\" : false, \"n\": null, \"i\":123, \"pi\": 3.1416, \"a\":[1, 2, 3, 4] } ";
MyHandler handler;
Reader reader;
StringStream ss(json);
reader.Parse(ss, handler);
}
ParseResult Parse(InputStream &is, Handler &handler)
JSONテキストを解析します。
Definition reader.h:485
RapidJSONの主な名前空間。
Definition rapidjson.h:374
GenericStringStream< UTF8< char > > StringStream
UTF8エンコーディングの文字列ストリーム。
Definition fwd.h:49
GenericReader< UTF8< char >, UTF8< char >, CrtAllocator > Reader
UTF8エンコーディングと既定のアロケーターを使うReader。
Definition fwd.h:90
Handlerの既定の実装。
Definition reader.h:196

RapidJSONは、仮想関数を持つクラスではなく、テンプレートでReader型とハンドラー型を静的に結び付けます。この方法では、関数のインライン化によって性能を改善できます。

Handler

前の例のように、Readerからのイベント(関数呼び出し)を受け取るハンドラーを実装する必要があります。ハンドラーには、以下のメンバー関数が必要です。

class Handler {
bool Null();
bool Bool(bool b);
bool Int(int i);
bool Uint(unsigned i);
bool Int64(int64_t i);
bool Uint64(uint64_t i);
bool Double(double d);
bool RawNumber(const Ch* str, SizeType length, bool copy);
bool String(const Ch* str, SizeType length, bool copy);
bool StartObject();
bool Key(const Ch* str, SizeType length, bool copy);
bool EndObject(SizeType memberCount);
bool StartArray();
bool EndArray(SizeType elementCount);
};
解析時にGenericReaderからイベントを受け取る概念。エラーがなければ関数はtrueを返し…(生成原文の抜粋)。
unsigned SizeType
大きさを表す型(文字列の長さ、配列の大きさなど)。
Definition rapidjson.h:380

ReaderがJSONのnull値に遭遇すると、Null()を呼びます。

ReaderがJSONのtrueかfalse値に遭遇すると、Bool(bool)を呼びます。

ReaderがJSONの数値に遭遇すると、適切なC++型への対応付けを選びます。そして、Int(int)、Uint(unsigned)、Int64(int64_t)、Uint64(uint64_t)、Double(double)のうち、1つの関数を呼びます。kParseNumbersAsStringsが有効なら、Readerは代わりに常にRawNumber()を呼びます。

Readerが文字列に遭遇すると、String(const char* str, SizeType length, bool copy)を呼びます。第1引数は文字列へのポインター、第2引数はヌル終端文字を含めない文字列の長さです。RapidJSONは、文字列内のヌル文字'\0'にも対応します。その場合は、strlen(str) < lengthとなります。最後のcopyは、ハンドラーが文字列をコピーする必要があるかを表します。通常の解析ではcopy = trueで、insitu解析の場合だけcopy = falseとなります。文字型は、この後で説明する変換先エンコーディングに依存することにも注意してください。

Readerがオブジェクトの開始に遭遇すると、StartObject()を呼びます。JSONのオブジェクトは、名前と値の組の集合です。メンバーがあれば、まず名前についてKey()を呼び、次に値の型に応じた関数を呼びます。名前と値の組についての呼び出しを、EndObject(SizeType memberCount)まで繰り返します。memberCount引数はハンドラーを助けるためのもので、必要ない場合もあります。

配列はオブジェクトと似ていますが、より単純です。配列の開始時に、ReaderはBeginArary()を呼びます。要素があれば、要素の型に応じた関数を呼びます。同様に、最後のEndArray(SizeType elementCount)のelementCount引数は、ハンドラーを助けるためのものです。

ハンドラーの各関数はboolを返します。通常はtrueを返します。エラーに遭遇した場合は、falseを返して、イベントの発行元へ処理の停止を伝えられます。

たとえば、ReaderでJSONを解析し、必要なスキーマにJSONが適合しないことをハンドラーが検出した場合、falseを返して、Readerの解析を停止できます。Readerは、エラーコードkParseErrorTerminationのエラー状態になります。

GenericReader

前述のように、ReaderはテンプレートクラスGenericReaderのtypedefです。

namespace rapidjson {
template <typename SourceEncoding, typename TargetEncoding, typename Allocator = MemoryPoolAllocator<> >
// ...
};
} // namespace rapidjson
SAX形式のJSONパーサー。UTF8エンコーディングと既定のアロケーターにはReaderを使います。
Definition reader.h:466
UTF-8エンコーディング。
Definition encodings.h:96

Readerは、入力と変換先の両方のエンコーディングにUTF-8を使います。入力エンコーディングはJSONストリームのエンコーディング、変換先はString()呼び出しのstr引数のエンコーディングです。たとえば、UTF-8ストリームを解析し、UTF-16文字列のイベントを出力するには、次のようにreaderを定義します。

UTF-16エンコーディング。
Definition encodings.h:269

UTF16の既定の文字型はwchar_tです。そのため、このreaderはハンドラーのString(const wchar_t*, SizeType, bool)を呼ぶ必要があります。

第3のテンプレート引数Allocatorは、内部データ構造(実際にはスタック)用のアロケーター型です。

解析

Readerの唯一の機能は、JSONの解析です。

template <unsigned parseFlags, typename InputStream, typename Handler>
bool Parse(InputStream& is, Handler& handler);
// with parseFlags = kDefaultParseFlags
template <typename InputStream, typename Handler>
bool Parse(InputStream& is, Handler& handler);

解析中にエラーが起こると、falseを返します。bool HasParseEror()、ParseErrorCode GetParseErrorCode()、size_t GetErrorOffset()でエラー状態を取得することもできます。実際に、DocumentはこれらのReaderの関数で解析エラーを取得しています。詳しくはDOMを参照してください。

Writer

ReaderはJSONをイベントへ変換(解析)します。Writerはその逆で、イベントをJSONへ変換します。

Writerはとても簡単に使えます。アプリケーションでデータをJSONへ変換するだけなら、Documentを作り、Writerで文字列化する代わりに、Writerを直接使う方法が適している場合があります。

simplewriterの例では、simplereaderのちょうど逆の処理を行います。

#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"
#include <iostream>
using namespace rapidjson;
using namespace std;
void main() {
writer.StartObject();
writer.Key("hello");
writer.String("world");
writer.Key("t");
writer.Bool(true);
writer.Key("f");
writer.Bool(false);
writer.Key("n");
writer.Null();
writer.Key("i");
writer.Uint(123);
writer.Key("pi");
writer.Double(3.1416);
writer.Key("a");
writer.StartArray();
for (unsigned i = 0; i < 4; i++)
writer.Uint(i);
writer.EndArray();
writer.EndObject();
cout << s.GetString() << endl;
}
JSONライター。
Definition writer.h:87
GenericStringBuffer< UTF8< char >, CrtAllocator > StringBuffer
UTF8エンコーディングの文字列バッファー。
Definition fwd.h:61
{"hello":"world","t":true,"f":false,"n":null,"i":123,"pi":3.1416,"a":[0,1,2,3]}

String()とKey()には、それぞれ2つのオーバーロードがあります。1つはハンドラーの概念で定義したものと同じ3引数のもので、ヌル文字を含む文字列を扱えます。もう1つは、上の例で使った単純なものです。

この例では、EndArray()とEndObject()に引数を渡していないことに注意してください。SizeTypeを渡すこともできますが、Writerはそれを単に無視します。

JSONを組み立てるのに、sprintf()やstd::stringstreamを使うだけではいけないのか、と思うかもしれません。

理由はいくつもあります。

  1. Writerは、整形式のJSONを出力しなければなりません。イベントの順序が不正な場合(StartObject()の直後にInt()など)、デバッグモードではアサーションに失敗します。
  2. Writer::String()は、文字列のエスケープ(コードポイントU+000Aを\nへ変換するなど)とUnicodeのエンコーディング変換を処理できます。
  3. Writerは、数値の出力を一貫して処理します。
  4. Writerはイベントハンドラーの概念を実装しています。Reader、Document、その他のイベント発行元からのイベントを処理できます。
  5. Writerは、プラットフォームごとに最適化できます。

いずれにせよ、WriterAPIは、その場限りの方法でJSONを生成するより簡単に使えます。

テンプレート

Writerには、Readerとの小さな設計上の違いがあります。Writerはtypedefではなくテンプレートクラスです。GenericWriterは存在しません。宣言は次のとおりです。

namespace rapidjson {
template<typename OutputStream, typename SourceEncoding = UTF8<>, typename TargetEncoding = UTF8<>, typename Allocator = CrtAllocator<>, unsigned writeFlags = kWriteDefaultFlags>
class Writer {
public:
Writer(OutputStream& os, Allocator* allocator = 0, size_t levelDepth = kDefaultLevelDepth)
// ...
};
} // namespace rapidjson
メモリーブロックの確保・大きさの変更・解放を行う概念。
Writer(OutputStream &os, StackAllocator *stackAllocator=0, size_t levelDepth=kDefaultLevelDepth)
コンストラクター。
Definition writer.h:99

テンプレート引数OutputStreamは、出力ストリームの型です。推論できないため、利用者が指定しなければなりません。

テンプレート引数SourceEncodingは、String(const Ch*, ...)で使うエンコーディングを指定します。

テンプレート引数TargetEncodingは、出力ストリームのエンコーディングを指定します。

Allocatorはアロケーターの型で、内部データ構造(スタック)のメモリー確保に使います。

writeFlagsは、次のビットフラグの組み合わせです。

解析フラグ意味
kWriteNoFlagsフラグを設定しません。
kWriteDefaultFlags既定の書き込みフラグです。マクロRAPIDJSON_WRITE_DEFAULT_FLAGSと同じ値で、このマクロはkWriteNoFlagsとして定義されています。
kWriteValidateEncodingFlagJSON文字列のエンコーディングの妥当性を検証します。
kWriteNanAndInfFlagInfinity、-Infinity、NaNの書き込みを許可します。

さらに、WriterのコンストラクターにはlevelDepth引数があります。この引数は、階層の各レベルの情報を保存するために最初に確保するメモリー量へ影響します。

PrettyWriter

Writerの出力は空白のない最もコンパクトなJSONで、ネットワーク転送や保存に適していますが、人には読みづらいものです。

そのため、RapidJSONは、出力にインデントと改行を加えるPrettyWriterを提供します。

PrettyWriterの使い方はWriterとまったく同じですが、PrettyWriterにはSetIndent(Ch indentChar, unsigned indentCharCount)関数があります。既定では、空白4個です。

完了状態とリセット

Writerは、ルートに任意のJSON型を持つ単一のJSONだけを出力できます。ルートの単独イベント(String()など)、または対応する最後のEndObject()かEndArray()イベントを処理すると、出力JSONは整形式で完全になります。Writer::IsComplete()でこの状態を検出できます。

JSONが完成すると、Writerは新しいイベントを受け取れません。受け取ると、ルートが複数ある不正な出力になります。Writerオブジェクトを再利用するには、Writer::Reset(OutputStream& os)を呼び、新しい出力ストリームとともにWriterの内部状態をすべてリセットできます。

テクニック

JSONを独自のデータ構造へ解析する

Documentの解析機能は、完全にReaderに基づいています。実際に、Documentはreaderからイベントを受け取り、解析中にDOMを組み立てるハンドラーです。

Readerで、他のデータ構造を直接組み立てることもできます。DOMを作らずに済むため、メモリーを減らし、性能を改善できます。

以下のmessagereaderの例では、ParseMessages()は、キーと文字列の組を持つオブジェクトとして想定されたJSONを解析します。

#include "rapidjson/error/en.h"
#include <iostream>
#include <string>
#include <map>
using namespace std;
using namespace rapidjson;
typedef map<string, string> MessageMap;
struct MessageHandler
: public BaseReaderHandler<UTF8<>, MessageHandler> {
MessageHandler() : state_(kExpectObjectStart) {
}
bool StartObject() {
switch (state_) {
case kExpectObjectStart:
state_ = kExpectNameOrObjectEnd;
return true;
default:
return false;
}
}
bool String(const char* str, SizeType length, bool) {
switch (state_) {
case kExpectNameOrObjectEnd:
name_ = string(str, length);
state_ = kExpectValue;
return true;
case kExpectValue:
messages_.insert(MessageMap::value_type(name_, string(str, length)));
state_ = kExpectNameOrObjectEnd;
return true;
default:
return false;
}
}
bool EndObject(SizeType) { return state_ == kExpectNameOrObjectEnd; }
bool Default() { return false; } // All other events are invalid.
MessageMap messages_;
enum State {
kExpectObjectStart,
kExpectNameOrObjectEnd,
kExpectValue,
}state_;
std::string name_;
};
void ParseMessages(const char* json, MessageMap& messages) {
Reader reader;
MessageHandler handler;
StringStream ss(json);
if (reader.Parse(ss, handler))
messages.swap(handler.messages_); // Only change it if success.
else {
size_t o = reader.GetErrorOffset();
cout << "Error: " << GetParseError_En(e) << endl;;
cout << " at offset " << o << " near '" << string(json).substr(o, 10) << "...'" << endl;
}
}
int main() {
MessageMap messages;
const char* json1 = "{ \"greeting\" : \"Hello!\", \"farewell\" : \"bye-bye!\" }";
cout << json1 << endl;
ParseMessages(json1, messages);
for (MessageMap::const_iterator itr = messages.begin(); itr != messages.end(); ++itr)
cout << itr->first << ": " << itr->second << endl;
cout << endl << "Parse a JSON with invalid schema." << endl;
const char* json2 = "{ \"greeting\" : \"Hello!\", \"farewell\" : \"bye-bye!\", \"foo\" : {} }";
cout << json2 << endl;
ParseMessages(json2, messages);
return 0;
}
ParseErrorCode GetParseErrorCode() const
直前の解析のParseErrorCodeを取得します。
Definition reader.h:534
size_t GetErrorOffset() const
入力内の直前の解析エラーの位置を取得します。エラーがなければ0です。
Definition reader.h:537
ParseErrorCode
解析のエラーコード。
Definition error.h:64
const RAPIDJSON_ERROR_CHARTYPE * GetParseError_En(ParseErrorCode parseErrorCode)
解析のエラーコードをエラーメッセージへ対応付けます。
Definition en.h:36
{ "greeting" : "Hello!", "farewell" : "bye-bye!" }
farewell: bye-bye!
greeting: Hello!
Parse a JSON with invalid schema.
{ "greeting" : "Hello!", "farewell" : "bye-bye!", "foo" : {} }
Error: Terminate parsing due to Handler error.
at offset 59 near '} }...'

最初のJSON(json1)は、MessageMapへ正常に解析されました。MessageMapはstd::mapなので、出力順はキーでソートされます。これはJSON内の順序とは異なります。

2つ目のJSON(json2)では、fooの値は空のオブジェクトです。オブジェクトなので、MessageHandler::StartObject()が呼ばれます。しかし、その時点ではstate_ = kExpectValueであるため、その関数はfalseを返し、解析を停止させます。エラーコードはkParseErrorTerminationです。

JSONのフィルタリング

前述のように、WriterはReaderが発行するイベントを処理できます。condenseの例は、ReaderのハンドラーとしてWriterを設定するだけで、JSONの空白をすべて削除できます。prettyの例も同じ関係を使い、WriterをPrettyWriterへ置き換えます。これにより、prettyでJSONにインデントと改行を付け直せます。

これらのSAX形式APIには、JSONの内容をフィルター処理する中間層を追加できます。たとえば、capitalizeの例はJSON内のすべての文字列を大文字にします。

#include "rapidjson/writer.h"
#include "rapidjson/filereadstream.h"
#include "rapidjson/filewritestream.h"
#include "rapidjson/error/en.h"
#include <vector>
#include <cctype>
using namespace rapidjson;
template<typename OutputHandler>
struct CapitalizeFilter {
CapitalizeFilter(OutputHandler& out) : out_(out), buffer_() {
}
bool Null() { return out_.Null(); }
bool Bool(bool b) { return out_.Bool(b); }
bool Int(int i) { return out_.Int(i); }
bool Uint(unsigned u) { return out_.Uint(u); }
bool Int64(int64_t i) { return out_.Int64(i); }
bool Uint64(uint64_t u) { return out_.Uint64(u); }
bool Double(double d) { return out_.Double(d); }
bool RawNumber(const char* str, SizeType length, bool copy) { return out_.RawNumber(str, length, copy); }
bool String(const char* str, SizeType length, bool) {
buffer_.clear();
for (SizeType i = 0; i < length; i++)
buffer_.push_back(std::toupper(str[i]));
return out_.String(&buffer_.front(), length, true); // true = output handler need to copy the string
}
bool StartObject() { return out_.StartObject(); }
bool Key(const char* str, SizeType length, bool copy) { return String(str, length, copy); }
bool EndObject(SizeType memberCount) { return out_.EndObject(memberCount); }
bool StartArray() { return out_.StartArray(); }
bool EndArray(SizeType elementCount) { return out_.EndArray(elementCount); }
OutputHandler& out_;
std::vector<char> buffer_;
};
int main(int, char*[]) {
// Prepare JSON reader and input stream.
Reader reader;
char readBuffer[65536];
FileReadStream is(stdin, readBuffer, sizeof(readBuffer));
// Prepare JSON writer and output stream.
char writeBuffer[65536];
FileWriteStream os(stdout, writeBuffer, sizeof(writeBuffer));
// JSON reader parse from the input stream and let writer generate the output.
CapitalizeFilter<Writer<FileWriteStream> > filter(writer);
if (!reader.Parse(is, filter)) {
fprintf(stderr, "\nError(%u): %s\n", (unsigned)reader.GetErrorOffset(), GetParseError_En(reader.GetParseErrorCode()));
return 1;
}
return 0;
}
fread()を使う入力用のファイルバイトストリーム。
Definition filereadstream.h:34
fread()を使う入力用のCファイルストリームのラッパー。
Definition filewritestream.h:32

JSON全体を単なる文字列として大文字にするのは不正です。たとえば、次のようになります。

["Hello\nWorld"]

JSON全体をそのまま大文字にすると、不正なエスケープ文字が含まれます。

["HELLO\NWORLD"]

capitalizeによる正しい結果:

["HELLO\nWORLD"]

さらに複雑なフィルターも作れます。ただし、SAX形式APIで得られる情報は一度に1イベント分だけなので、文脈情報(ルート値からのパスや関連する他の値の保存など)を管理する必要がある場合があります。処理によっては、SAXよりDOMの方が実装しやすい場合もあります。