(この機能はv1.1.0で公開されました)

JSON Pointerは、JSONドキュメント(DOM)内の値を選択するための標準化された方法(RFC6901)です。XMLドキュメントのXPathに似ていますが、JSON Pointerははるかに単純で、一つのJSON Pointerは一つの値だけを指します。

RapidJSONのJSON Pointer実装を使うと、DOMの一部の操作を簡単にできます。

JSON Pointer

JSON Pointerは、各トークンの先頭に/を付けた、0個以上のトークンのリストです。各トークンは文字列か数値です。たとえば、次のJSONを考えます。

{
"foo" : ["bar", "baz"],
"pi" : 3.1416
}

以下のJSON Pointerは、このJSONに対して次の値を指します。

  1. "/foo" → [ "bar", "baz" ]
  2. "/foo/0" → "bar"
  3. "/foo/1" → "baz"
  4. "/pi" → 3.1416

空のJSON Pointer ""(トークン数0)は、JSON全体を指すことに注意してください。

基本的な使い方

次のコード例を見れば、使い方が分かります。

#include "rapidjson/pointer.h"
// ...
Document d;
// Create DOM by Set()
Pointer("/project").Set(d, "RapidJSON");
Pointer("/stars").Set(d, 10);
// { "project" : "RapidJSON", "stars" : 10 }
// Access DOM by Get(). It return nullptr if the value does not exist.
if (Value* stars = Pointer("/stars").Get(d))
stars->SetInt(stars->GetInt() + 1);
// { "project" : "RapidJSON", "stars" : 11 }
// Set() and Create() automatically generate parents if not exist.
Pointer("/a/b/0").Create(d);
// { "project" : "RapidJSON", "stars" : 11, "a" : { "b" : [ null ] } }
// GetWithDefault() returns reference. And it deep clones the default value.
Value& hello = Pointer("/hello").GetWithDefault(d, "world");
// { "project" : "RapidJSON", "stars" : 11, "a" : { "b" : [ null ] }, "hello" : "world" }
// Swap() is similar to Set()
Value x("C++");
Pointer("/hello").Swap(d, x);
// { "project" : "RapidJSON", "stars" : 11, "a" : { "b" : [ null ] }, "hello" : "C++" }
// x becomes "world"
// Erase a member or element, return true if the value exists
bool success = Pointer("/a").Erase(d);
assert(success);
// { "project" : "RapidJSON", "stars" : 10 }
ValueType & Create(ValueType &root, typename ValueType::AllocatorType &allocator, bool *alreadyExist=0) const
部分木内に値を作成します。
Definition pointer.h:401
ValueType & GetWithDefault(ValueType &root, const ValueType &defaultValue, typename ValueType::AllocatorType &allocator) const
部分木内の値を問い合わせ、既定値も指定します。
Definition pointer.h:533
ValueType & Swap(ValueType &root, ValueType &value, typename ValueType::AllocatorType &allocator) const
値を部分木内の値と交換します。
Definition pointer.h:692
bool Erase(ValueType &root) const
部分木内の値を削除します。
Definition pointer.h:711
ValueType & Set(ValueType &root, ValueType &value, typename ValueType::AllocatorType &allocator) const
ムーブセマンティクスを使って、部分木内の値を設定します。
Definition pointer.h:610

ヘルパー関数

オブジェクト指向の呼び出し方は直感的でない場合があるため、RapidJSONには、メンバー関数を自由関数で包んだヘルパー関数もあります。

次の例は、前の例とまったく同じ処理を行います。

Document d;
SetValueByPointer(d, "/project", "RapidJSON");
SetValueByPointer(d, "/stars", 10);
if (Value* stars = GetValueByPointer(d, "/stars"))
stars->SetInt(stars->GetInt() + 1);
CreateValueByPointer(d, "/a/b/0");
Value& hello = GetValueByPointerWithDefault(d, "/hello", "world");
Value x("C++");
SwapValueByPointer(d, "/hello", x);
bool success = EraseValueByPointer(d, "/a");
assert(success);

比較のため、呼び出し方を並べます。

  1. Pointer(source).<Method>(root, ...)
  2. <Method>ValueByPointer(root, Pointer(source), ...)
  3. <Method>ValueByPointer(root, source, ...)

ポインターの解決

Pointer::Get()やGetValueByPointer()は、DOMを変更しません。トークンに一致する値がDOM内になければ、nullptrを返します。これを使って、値が存在するかを確認できます。

数値のトークンは、配列の添字にもメンバー名にもなります。解決処理は、値の型に応じて値を照合します。

{
"0" : 123,
"1" : [456]
}
  1. "/0" → 123
  2. "/1/0" → 456

トークン"0"は、最初のポインターではメンバー名、2番目のポインターでは配列の添字として扱われます。

Create()、GetWithDefault()、Set()、Swap()などのほかの関数は、DOMを変更します。これらの関数は常に成功します。親の値が存在しなければ作成し、親の値がトークンに一致しなければ、その型を強制的に変更します。型を変更すると、そのDOMの部分木は完全に削除されます。

上のJSONをdへ解析すると、次のようになります。

SetValueByPointer(d, "1/a", 789); // { "0" : 123, "1" : { "a" : 789 } }

マイナス記号トークンの解決

また、RFC6901は、配列の末尾の次の要素を表す特別なトークン-(マイナス記号一つ)を定義しています。Get()は、このトークンをメンバー名「"-"」としてだけ扱います。一方、ほかの関数は配列に対して解決でき、配列のValue::PushBack()を呼ぶのと同じになります。

Document d;
d.Parse("{\"foo\":[123]}");
SetValueByPointer(d, "/foo/-", 456); // { "foo" : [123, 456] }
SetValueByPointer(d, "/-", 789); // { "foo" : [123, 456], "-" : 789 }
GenericDocument & Parse(const typename SourceEncoding::Ch *str)
読み取り専用の文字列から、エンコーディング変換を伴ってJSONテキストを解析します。
Definition document.h:2221

DocumentとValueに対する解決

p.Get(root)やGetValueByPointer(root, p)を使う場合、rootは(constの)Value&です。つまり、DOMの部分木でもかまいません。

ほかの関数には、2種類のシグネチャがあります。一方は引数にDocument& documentを使い、もう一方はValue& rootを使います。前者は値の作成にdocument.GetAllocator()を使います。後者は、DOMの関数と同様に、利用者がアロケーターを渡す必要があります。

上の例はすべて、第1引数がDocument&なので、アロケーター引数は不要です。ただし、ポインターを部分木に対して解決する場合は、次の例のようにアロケーターを渡す必要があります。

class Person {
public:
Person() {
document_ = new Document();
// CreateValueByPointer() here no need allocator
SetLocation(CreateValueByPointer(*document_, "/residence"), ...);
SetLocation(CreateValueByPointer(*document_, "/office"), ...);
};
private:
void SetLocation(Value& location, const char* country, const char* addresses[2]) {
Value::Allocator& a = document_->GetAllocator();
// SetValueByPointer() here need allocator
SetValueByPointer(location, "/country", country, a);
SetValueByPointer(location, "/address/0", address[0], a);
SetValueByPointer(location, "/address/1", address[1], a);
}
// ...
Document* document_;
};
GenericDocument< UTF8<> > Document
UTF8エンコーディングのGenericDocument。
Definition document.h:2402

Erase()やEraseValueByPointer()は、アロケーターを必要としません。値を正常に削除すると、trueを返します。

エラー処理

Pointerは、コンストラクターで入力文字列を解析します。解析エラーがあると、Pointer::IsValid()はfalseを返します。Pointer::GetParseErrorCode()とGetParseErrorOffset()で、エラー情報を取得できます。

すべての解決関数は、有効なポインターを前提とすることに注意してください。無効なポインターで解決すると、アサーションに失敗します。

URIフラグメント表現

ここまで使ってきたJSON Pointerの文字列表現に加え、RFC6901はURIフラグメント表現も定義しています。URIフラグメントは、RFC3986「Uniform Resource Identifier (URI): Generic Syntax」で規定されています。

主な違いは、URIフラグメントの先頭に必ず#(番号記号)が付き、一部の文字はUTF-8のバイト列をパーセントエンコードして表すことです。次の表は、各表現のC/C++文字列リテラルを示します。

文字列表現URIフラグメント表現ポインターのトークン(UTF-8)
"/foo/0""#/foo/0"{"foo", 0}
"/a~1b""#/a~1b"{"a/b"}
"/m~0n""#/m~0n"{"m~n"}
"/ ""#/%20"{" "}
"/\0""#/%00"{"\0"}
"/€""#/%E2%82%AC"{"€"}

RapidJSONはURIフラグメント表現を完全にサポートし、解析時に番号記号を自動検出します。

文字列化

Pointerを文字列やほかの出力ストリームへ文字列化することもできます。方法は次のとおりです。

Pointer p(...);
StringBuffer sb;
p.Stringify(sb);
std::cout << sb.GetString() << std::endl;

StringifyUriFragment()で、URIフラグメント表現へ文字列化することもできます。

利用者が指定するトークン

ポインターを何度も解決する場合は、一度だけ構築し、異なるDOMや異なる時点で使うようにします。これにより、Pointerを何度も構築する時間とメモリー確保を減らせます。

さらに、トークン配列を直接作ることで、解析処理と動的なメモリー確保を完全になくせます。

#define NAME(s) { s, sizeof(s) / sizeof(s[0]) - 1, kPointerInvalidIndex }
#define INDEX(i) { #i, sizeof(#i) - 1, i }
static const Pointer::Token kTokens[] = { NAME("foo"), INDEX(123) };
static const Pointer p(kTokens, sizeof(kTokens) / sizeof(kTokens[0]));
// Equivalent to static const Pointer p("/foo/123");

これは、メモリーに制約のあるシステムで役立つ場合があります。