JSONスキーマでJSONのバリデーション

JSON形式のデータがある時、そのデータがあらかじめ決められたルールを守って書かれているかどうかをチェックしたい場合があります。このような目的のためにJSONスキーマという規格が策定されています。

## JSONスキーマとは

例として、次のようなJSON形式のデータを考えます。

```json:person.json
{
    "name": "山田太郎",
    "age": 42,
    "hobbies": ["野球", "柔道"]
}
```

JSONスキーマでは、バリデーションの対象となるこのようなJSON形式のデータをJSONインスタンスと呼びます。ここでは、「人」を表す上記のJSONインスタンスに対して、次のような制約を設けたいとします。

* `name`プロパティの値の型は文字列
* `age`プロパティの値の型は0以上の整数
* `hobbies`プロパティの値の型は配列で、その要素の型は文字列
* `name`プロパティは省略不可

JSONスキーマではこのような制約を次のように表現します。JSONスキーマ自体もJSON形式で記述します。

```json:person.schema.json
{
    "type": "object",
    "properties": {
        "name": {
            "type": "string"
        },
        "age": {
            "type": "integer",
            "minimum": 0
        },
        "hobbies": {
            "type": "array",
            "items": {
                "type": "string"
            }
        }
    },
    "required": ["name"]
}
```

JSONスキーマの仕様は[json-schema-org]によって策定されています。本記事執筆時点での最新仕様は[Draft-07]です。

## JSONをバリデーションしてみる

それでは実際にJSONインスタンスをJSONスキーマにしたがってバリデーションしてみましょう。以降のコードはJavaで記述します。

本記事では、JSONのバリデーションを行うライブラリとして、[Justify]を使用します。このライブラリでは、JSONインスタンスの読み込みに[Java API for JSON Processing (JSR 374)]が提供する`javax.json`パッケージのインタフェースをそのまま使います。このため、APIを使う側はバリデーションしていることをあまり意識する必要がありません。

## 準備

バリデーションを行うJavaアプリケーションへ依存ファイルとして以下の2つを追加します。ビルドツールとして[Maven]を使った場合の例です。

```xml:pom.xml
<!-- ライブラリ本体 -->
<dependency>
  <groupId>org.leadpony.justify</groupId>
  <artifactId>justify</artifactId>
  <version>2.0.0</version>
</dependency>

<!-- Java API for JSON Processingの実装 -->
<dependency>
  <groupId>org.glassfish</groupId>
  <artifactId>jakarta.json</artifactId>
  <classifier>module</classifier>
  <version>1.1.6</version>
</dependency>
```

2つめの依存ファイルは、[Java API for JSON Processing (JSR 374)]の実装としてその[リファレンス実装]を用いた場合の例です。[Apache Johnzon]などの互換性のある別の実装に差し替えても問題ありません。

## ストリーミングAPIでバリデーション

[Java API for JSON Processing (JSR 374)]には、ストリーミングAPIとオブジェクトモデルAPIの2種類のAPIがあります。最初にストリーミングAPIを使ってJSONインスタンスをバリデーションしてみましょう。

ストリーミングAPIでは、オブジェクトの始まり・プロパティの名前・プロパティの値…といったJSONの読み込み中に発生する細かなイベント単位で呼出し側に制御が戻ります。JSONインスタンスの読み込みには`javax.json.stream.JsonParser`インタフェースを使用します。

```java
import java.nio.file.Paths;
import javax.json.stream.JsonParser;
import org.leadpony.justify.api.JsonSchema;
import org.leadpony.justify.api.JsonValidationService;
import org.leadpony.justify.api.ProblemHandler;

// バリデーションサービスを作成する。
JsonValidationService service = JsonValidationService.newInstance();
// ファイルからJSONスキーマを読み込む。
JsonSchema schema = service.readSchema(Paths.get("person.schema.json"));
// バリデーション中に見つかった問題を処理するハンドラ。
// ここでは標準出力に出力するのみ。
ProblemHandler handler = service.createProblemPrinter(System.out::println);

// JSONインスタンスを読み込むパーサーを作成する。
try (JsonParser parser = service.createParser(Paths.get("person.json"), schema, handler)) {
    // すべてのパーサーイベントを処理する
    while (parser.hasNext()) {
        // 発生したパーサーイベント
        JsonParser.Event event = parser.next();
        // イベントを処理する。
        doSomethingUseful(event);
    }
}
```
試しに、次のように不正なJSONインスタンスを入力してみます。

```json:person-bad.json
{
    "age": -1,
    "hobbies": ["野球", "柔道"]
}
```
バリデーション中に見つかった問題は次のように出力されます。

```
[2行,13列] 数値は0以上でなければいけません。
[4行,1列] オブジェクトはプロパティ"name"を持たなければいけません。
```

実行時のロケールが日本語の場合はエラーメッセージも日本語になります。

## オブジェクトモデルAPIでバリデーション

続いてオブジェクトモデルAPIを使ったコード例を見てみましょう。

オブジェクトモデルAPIでは、JSONの読み込みによって、JSONのオブジェクトや配列全体がメモリ上に構築されます。JSONインスタンスの読み込みには`javax.json.JsonReader`インタフェースを使用します。

```java
import java.nio.file.Paths;
import javax.json.JsonObject;
import javax.json.JsonReader;
import org.leadpony.justify.api.JsonSchema;
import org.leadpony.justify.api.JsonValidationService;
import org.leadpony.justify.api.ProblemHandler;

// バリデーションサービスを作成する。
JsonValidationService service = JsonValidationService.newInstance();
// ファイルからJSONスキーマを読み込む。
JsonSchema schema = service.readSchema(Paths.get("person.schema.json"));
// バリデーション中に見つかった問題を処理するハンドラ。
// ここでは標準出力に出力するのみ。
ProblemHandler handler = service.createProblemPrinter(System.out::println);

// JSONインスタンスを読み込むリーダーを作成する。
try (JsonReader reader = service.createReader(Paths.get("person.json"), schema, handler)) {
    // JSONオブジェクトを読み込む
    JsonObject object = reader.readObject();
    // 読み込んだJSONオブジェクトを処理する。
    doSomethingUseful(object);
}
```

オブジェクトモデルAPIの場合でも、JSONインスタンスに問題が見つかった時は、ストリーミングAPIと同様なエラーメッセージが出力されます。

## バインディングAPIでバリデーション

Javaには、JSONインスタンスをダイレクトにPOJO (Plain Old Java Object)へ変換するためのAPI仕様として、[Java API for JSON Binding (JSR 367)]があります。最後に、このAPIを使ってJSONインスタンスをバリデーションしてみましょう。

[Java API for JSON Binding (JSR 367)]の実装として[Yasson]を使用するため、これを依存ファイルとして追加します。

```xml:pom.xml
<!-- Java API for JSON Bindingの実装 -->
<dependency>
  <groupId>org.eclipse</groupId>
  <artifactId>yasson</artifactId>
  <version>1.0.2</version>
</dependency>
```

次に、変換先のPOJOとして`Person`クラスを定義します。

```java
public class Person {
    public String name;
    public int age;
    public List<String> hobbies;
}
```

バリデーションのコードは次のようになります。

```java
import java.nio.file.Paths;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.spi.JsonProvider;
import org.leadpony.justify.api.JsonSchema;
import org.leadpony.justify.api.JsonValidationService;

// バリデーションサービスを作成する。
JsonValidationService service = JsonValidationService.newInstance();
// ファイルからJSONスキーマを読み込む。
JsonSchema schema = service.readSchema(Paths.get("person.schema.json"));
// JsonProviderのインスタンスを作成する。
JsonProvider provider = service.createJsonProvider(
        schema, 
        parser->service.createProblemPrinter(System.out::println));
// Jsonbのインスタンスを作成する。
Jsonb jsonb = JsonbBuilder.newBuilder().withProvider(provider).build();

// JSONインスタンスを入力ストリームとして開く。
try (InputStream stream = Files.newInputStream(Paths.get("person.json"))) {
    // JSONインスタンスをPersonオブジェクトに変換する。
    Person person = jsonb.fromJson(stream, Person.class);
    // Personオブジェクトを処理する。　
    doSomethingUseful(person);
}
```

これまで紹介したコードと同様に、入力したJSONインスタンスに問題が見つかった場合は、エラーメッセージが出力されます。

## その他のバリデータ実装

JSONスキーマ仕様をベースにしたJSONのバリデータに関しては、Java以外のプログラミング言語用を含め、json-schema.orgの[Implementations]に最新リストが提示されています。なお、本記事で紹介した[Justify]は私自身が開発したものです。


[json-schema-org]: https://json-schema.org/
[Draft-07]: https://json-schema.org/specification.html
[Maven]: https://maven.apache.org/
[Java API for JSON Processing (JSR 374)]: https://eclipse-ee4j.github.io/jsonp/
[Java API for JSON Binding (JSR 367)]: http://json-b.net/
[リファレンス実装]: https://github.com/eclipse-ee4j/jsonp
[Apache Johnzon]: https://johnzon.apache.org/
[Yasson]: https://github.com/eclipse-ee4j/yasson
[Implementations]: https://json-schema.org/implementations.html
[Justify]: https://github.com/leadpony/justify