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

JSON Schemaは、JSONデータの形式を記述するための標準の草案です。スキーマ自体もJSONデータです。JSON SchemaでJSON構造を検証すると、型やキーの存在などを手作業で確認せずに、コードからDOMへ安全にアクセスできます。また、シリアライズしたJSONが、指定したスキーマに適合することも保証できます。

RapidJSONには、JSON Schema Draft v4のバリデーターが実装されています。JSON Schemaに詳しくない場合は、Understanding JSON Schemaを参照してください。

基本的な使い方

まず、JSON SchemaをDocumentへ解析し、そのDocumentをSchemaDocumentへコンパイルします。

次に、そのSchemaDocumentを使ってSchemaValidatorを構築します。SAXイベントを処理するという点で、Writerと似ています。したがって、document.Accept(validator)でドキュメントを検証し、その妥当性を確認できます。

#include "rapidjson/schema.h"
// ...
Document sd;
if (!sd.Parse(schemaJson).HasParseError()) {
// the schema is not a valid JSON.
// ...
}
SchemaDocument schema(sd); // Compile a Document to SchemaDocument
// sd is no longer needed here.
Document d;
if (!d.Parse(inputJson).HasParseError()) {
// the input is not a valid JSON.
// ...
}
SchemaValidator validator(schema);
if (!d.Accept(validator)) {
// Input JSON is invalid according to the schema
// Output diagnostic information
validator.GetInvalidSchemaPointer().StringifyUriFragment(sb);
printf("Invalid schema: %s\n", sb.GetString());
printf("Invalid keyword: %s\n", validator.GetInvalidSchemaKeyword());
sb.Clear();
validator.GetInvalidDocumentPointer().StringifyUriFragment(sb);
printf("Invalid document: %s\n", sb.GetString());
}
GenericStringBuffer< UTF8< char >, CrtAllocator > StringBuffer
UTF8エンコーディングの文字列バッファー。
Definition fwd.h:61

補足:

  • 一つのSchemaDocmentを、複数のSchemaValidatorが参照できます。SchemaValidatorは、それを変更しません。
  • SchemaValidatorは、複数のドキュメントの検証に再利用できます。別のドキュメントで使う前に、validator.Reset()を呼びます。

解析・シリアライズ中の検証

多くのJSON Schemaバリデーター実装と異なり、RapidJSONはSAXに基づくスキーマバリデーターを提供します。そのため、ストリームからJSONを解析しながら、その場で検証できます。指定したスキーマに違反するJSONの値に遭遇すると、解析は直ちに終了します。この設計は、大きなJSONファイルの解析に特に役立ちます。

DOMによる解析

DOMを使って解析する場合、Documentには、SAXイベントの受信に加え、準備と終了処理が必要です。そのため、reader・バリデーター・ドキュメントの間でイベントを受け渡すための処理が必要になります。SchemaValidatingReaderは、この処理を行うヘルパークラスです。

#include "rapidjson/filereadstream.h"
// ...
SchemaDocument schema(sd); // Compile a Document to SchemaDocument
// Use reader to parse the JSON
FILE* fp = fopen("big.json", "r");
FileReadStream is(fp, buffer, sizeof(buffer));
// Parse JSON from reader, validate the SAX events, and store in d.
Document d;
SchemaValidatingReader<kParseDefaultFlags, FileReadStream, UTF8<> > reader(is, schema);
d.Populate(reader);
if (!reader.GetParseResult()) {
// Not a valid JSON
// When reader.GetParseResult().Code() == kParseErrorTermination,
// it may be terminated by:
// (1) the validator found that the JSON is invalid according to schema; or
// (2) the input stream has I/O error.
// Check the validation result
if (!reader.IsValid()) {
// Input JSON is invalid according to the schema
// Output diagnostic information
StringBuffer sb;
reader.GetInvalidSchemaPointer().StringifyUriFragment(sb);
printf("Invalid schema: %s\n", sb.GetString());
printf("Invalid keyword: %s\n", reader.GetInvalidSchemaKeyword());
sb.Clear();
reader.GetInvalidDocumentPointer().StringifyUriFragment(sb);
printf("Invalid document: %s\n", sb.GetString());
}
}

SAXによる解析

SAXを使った解析は、はるかに単純です。JSONを検証するだけで、その後の処理が不要なら、次のように書けます。

SchemaValidator validator(schema);
Reader reader;
if (!reader.Parse(stream, validator)) {
if (!validator.IsValid()) {
// ...
}
}

これは、schemavalidatorの例で使われている方法そのものです。明確な利点は、JSONの大きさにかかわらずメモリー使用量が少ないことです(メモリー使用量はスキーマの複雑さに依存します)。

さらにSAXイベントを処理する必要がある場合は、テンプレートクラスGenericSchemaValidatorを使い、バリデーターの出力ハンドラーを設定します。

MyHandler handler;
GenericSchemaValidator<SchemaDocument, MyHandler> validator(schema, handler);
Reader reader;
if (!reader.Parse(ss, validator)) {
if (!validator.IsValid()) {
// ...
}
}

シリアライズ

シリアライズ中に検証することもできます。これにより、出力JSONがJSON Schemaに適合することを保証できます。

StringBuffer sb;
Writer<StringBuffer> writer(sb);
GenericSchemaValidator<SchemaDocument, Writer<StringBuffer> > validator(s, writer);
if (!d.Accept(validator)) {
// Some problem during Accept(), it may be validation or encoding issues.
if (!validator.IsValid()) {
// ...
}
}

もちろん、アプリケーションにSAX形式のシリアライズだけが必要なら、Writerの代わりにSchemaValidatorへSAXイベントを送るだけで済みます。

リモートスキーマ

JSON Schemaは、ローカルまたはリモートのスキーマを参照するJSON Pointerである、$ref キーワードをサポートします。ローカルのポインターは#で始まり、リモートのポインターは相対または絶対URIです。例を示します。

{ "$ref": "definitions.json#/address" }

SchemaDocumentには、このようなURIを解決する方法が分からないため、利用者が提供するIRemoteSchemaDocumentProviderのインスタンスが必要です。

class MyRemoteSchemaDocumentProvider : public IRemoteSchemaDocumentProvider {
public:
virtual const SchemaDocument* GetRemoteDocument(const char* uri, SizeTyp length) {
// Resolve the uri and returns a pointer to that schema.
}
};
// ...
MyRemoteSchemaDocumentProvider provider;
SchemaDocument schema(sd, &provider);

適合性

RapidJSONは、JSON Schema Test Suite(JSON Schema draft 4)の263テスト中262テストに合格しました。

失敗したのは、refRemote.jsonの「change resolution scope」にある「changed scope ref invalid」テストです。スキーマのキーワードidと、URIを結合する機能が実装されていないためです。

また、文字列の値に対するスキーマのキーワードformatは、仕様で必須とされていないため無視されます。

正規表現

スキーマのキーワードpatternとpatternPropertiesは、正規表現で要求するパターンを照合します。

RapidJSONには単純なNFA正規表現エンジンが実装されており、既定で使われます。次の構文に対応しています。

構文説明
ab連結
a|b選択
a?0回または1回
a*0回以上
a+1回以上
a{3}ちょうど3回
a{3,}3回以上
a{3,5}3回から5回
(ab)グループ化
^a先頭
a$末尾
.任意の文字
[abc]文字クラス
[a-c]文字クラスの範囲
[a-z0-9_]文字クラスの組み合わせ
[^abc]否定文字クラス
[^a-c]否定文字クラスの範囲
[\b]バックスペース(U+0008)
\|, \\, ...文字のエスケープ
\f改ページ(U+000C)
\n改行(U+000A)
\r復帰(U+000D)
\tタブ(U+0009)
\v垂直タブ(U+000B)

C++11コンパイラーでは、RAPIDJSON_SCHEMA_USE_INTERNALREGEX=0とRAPIDJSON_SCHEMA_USE_STDREGEX=1を定義することで、std::regexも使えます。スキーマでpatternとpatternPropertiesが不要なら、両方のマクロを0にしてこの機能を無効にすると、コードサイズをいくらか小さくできます。

性能

C++のJSONライブラリーの多くは、まだJSON Schemaに対応していません。そこで、Node.js上で動く11のJavaScriptライブラリーをテストするjson-schema-benchmarkに合わせて、RapidJSONのJSON Schemaバリデーターの性能を評価しました。

このベンチマークは、JSON Schema Test Suiteを使って検証を実行しますが、一部のテストスイートとテストを除外しています。schematest.cppに、同じベンチマーク手順を実装しました。

Mac Book Pro(2.8 GHz Intel Core i7)で、次の結果が得られました。

バリデーター相対速度1秒当たりのテスト実行回数
RapidJSON155%30682
ajv100%19770 (± 1.31%)
is-my-json-valid70%13835 (± 2.84%)
jsen57.7%11411 (± 1.27%)
schemasaurus26%5145 (± 1.62%)
themis19.9%3935 (± 2.69%)
z-schema7%1388 (± 0.84%)
jsck3.1%606 (± 2.84%)
jsonschema0.9%185 (± 1.01%)
skeemas0.8%154 (± 0.79%)
tv40.5%93 (± 0.94%)
jayschema0.1%21 (± 1.14%)

つまり、RapidJSONは最も速いJavaScriptライブラリー(ajv)の約1.5倍、最も遅いものの1400倍の速さです。