0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スキーマ定義を用いたJSONバリデーション

Posted at

はじめに

公開している Web API 仕様と実際の実装に差が出ないようにするにはどうすればいいのでしょうか? いろいろ考えた結果、現在開発中の新製品では次の方法を取ることにしました。

手順
1 Web API 仕様を OpenAPI で記述する
2 その OpenAPI ドキュメントの内容を MicroProfile OpenAPI 仕様に従って /openapi エンドポイントで公開する
3 Web API の実装では、その OpenAPI ドキュメントを用いてリクエストボディのバリデーションを行う

この記事では、上記の最後の手順に相当する、「スキーマ定義ファイルを用いて JSON バリデーションを行う」方法について簡易実装を紹介します。

実装から OpenAPI ドキュメントを生成する方法があることは知っていますが、仕組み上、満足できるドキュメントが生成されそうもなかったので、その方法は取らないことにしました。

スキーマ定義を用いたバリデーションの実装

ここで紹介する実装では json-schema-validator ライブラリを用います。pom.xmlcom.networknt:json-schema-validator を追加することで当ライブラリを利用できます。

pom.xml に追加する内容
<dependency>
    <groupId>com.networknt</groupId>
    <artifactId>json-schema-validator</artifactId>
    <version>2.0.0</version>
</dependency>

json-schema-validator ライブラリは、バージョン 1.5.x からバージョン 2.0.0 へのアップグレードの際、かなりの量の破壊的変更をおこないました。詳細は Migration to 2.0.0 from 1.5.x を参照してください。

バリデーションの核心部分は、スキーマ定義ファイルから Schema インスタンスを作成し、そのインスタンスの validate メソッドにバリデーション対象の JSON を渡す、という処理です。

バリデーションの核心部分
// スキーマ定義ファイルの内容から Schema インスタンスを作成する。
Schema schema = registry.getSchema(SchemaLocation.of(schemaFile));

// バリデーションを実行する。バリデーションエラーがなければ、
// 空の配列が返される。
List<Error> errors = schema.validate(input, InputFormat.JSON);

これを念頭に、下記のプログラム全体をご覧ください。(分かりやすさのため例外処理はしていません。)

SchemaValidation.java
SchemaValidation.java
package com.example;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import com.networknt.schema.Error;
import com.networknt.schema.InputFormat;
import com.networknt.schema.Schema;
import com.networknt.schema.SchemaLocation;
import com.networknt.schema.SchemaRegistry;
import com.networknt.schema.SchemaRegistryConfig;
import com.networknt.schema.SpecificationVersion;
import com.networknt.schema.path.PathType;

public class SchemaValidation
{
    public static void main(String[] args) throws Exception
    {
        // スキーマ定義ファイルの名前。クラスパスがファイル検索場所となるので、
        // "resource:/schema/stream-status.yaml" などと指定する。
        String schemaFile = args[0];

        // バリデーション対象となる入力ファイル。このコード例では
        // ファイルシステムをファイル検索場所としている。
        String inputFile = args[1];
        String input     = Files.readString(Path.of(inputFile));

        // 入力ファイルの内容がスキーマ定義ファイルに記述されている制約に
        // 従っているかどうか調べる。従っていれば true が返される。
        // 従っていなければエラーメッセージが出力され、false が返される。
        boolean passed = validate(schemaFile, input);

        // バリデーションの結果を出力する。
        System.out.format("validation result = %s%n", passed);
    }

    private static boolean validate(String schemaFile, String input)
    {
        // SchemaRegistry のための設定を用意する。
        SchemaRegistryConfig config =
            SchemaRegistryConfig.builder()
                .pathType(PathType.JSON_POINTER)
                .locale(Locale.ROOT)
                .build();

        // SchemaRegistry を取得する。
        SchemaRegistry registry =
            SchemaRegistry.withDefaultDialect(
                SpecificationVersion.DRAFT_2020_12,
                builder -> builder.schemaRegistryConfig(config));

        // スキーマ定義ファイルの内容から Schema インスタンスを作成する。
        Schema schema = registry.getSchema(SchemaLocation.of(schemaFile));

        // バリデーションを実行する。バリデーションエラーがなければ、
        // 空の配列が返される。
        List<Error> errors = schema.validate(input, InputFormat.JSON);

        // バリデーションエラーが発生していれば、エラーメッセージを出力する。
        for (Error error : errors)
        {
            System.err.println(error);
        }

        // バリデーションエラーの配列が空であれば、バリデーションをパスしたということ。
        boolean passed = errors.isEmpty();

        // バリデーションの結果を返す。true ならばバリデーションをパスしたということ。
        return passed;
    }
}

このプログラムは、コマンドラインの第一引数にスキーマ定義ファイルのパス、第二引数にバリデーション対象の JSON を含むファイルのパス、を取ります。

バリデーション動作確認作業用のファイル

まず、プログラムを mvn exec:java で実行するための pom.xml ファイルを用意します。

pom.xml
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>schema-validation</artifactId>
    <version>1.0.0</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <json-schema-validator.version>2.0.0</json-schema-validator.version>
        <maven-compiler-plugin.version>3.14.1</maven-compiler-plugin.version>
        <maven-compiler-plugin.release>11</maven-compiler-plugin.release>
        <slf4.version>2.0.17</slf4.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.networknt</groupId>
            <artifactId>json-schema-validator</artifactId>
            <version>${json-schema-validator.version}</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>${slf4.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven-compiler-plugin.version}</version>
                <configuration>
                    <release>${maven-compiler-plugin.release}</release>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

次に、スキーマ定義ファイルのサンプルとして stream-status.yaml を用意します。

stream-status.yaml
stream-status.yaml
type: object
properties:
  stream_id:
    description: "The stream ID"
    type: string

  status:
    description: "The stream status"
    type: string
    enum:
      - "enabled"
      - "paused"
      - "disabled"

  reason:
    description: "The reason for the current status"
    type: string

required:
  - stream_id
  - status

example:
  stream_id: "stj3odpcfg19u6q62t2h7k0uk3t7srkdai6e0ccd7jd4ognt"
  status: "enabled"
  reason: "The initial status"

このスキーマ定義は、OpenID Shared Signals Framework Specification 1.0 (SSF) で定義されている Stream Status Update API が受け取るリクエストを表しています。定義内容を表にすると次の通りです。

パラメータ 要否 説明
stream_id 必須 string ストリーム ID
status 必須 string 更新後のステータス
有効な値は enabledpauseddisabled
reason 任意 string ステータス更新の理由

そして、テスト用の入力ファイルを三つ用意します。

status_enabled.json — 正しいデータ
{
    "stream_id": "stream_id_1",
    "status": "enabled"
}
status_missing.json — 必須パラメータの status を含んでいないデータ
{
    "stream_id": "stream_id_1"
}
status_unknown.jsonstatus パラメータの値が無効のデータ
{
    "stream_id": "stream_id_1",
    "status": "unknown"
}

また、テストプログラム SchemaValidation を起動するためのシェルスクリプト run.sh を用意します。

run.sh
run.sh
#!/bin/sh

# 第一引数にスキーマ定義ファイルのパス (クラスパス上で検索可能なファイル名)、
# 第二引数に入力ファイルのパス (ファイルシステム上で検索可能なファイル名) を
# 指定し、com.example.SchemaValidation プログラムを起動する。
#
# 注:
#   このシェルスクリプトに対する第一引数 (${1}) を、SchemaValidation
#   プログラムの第二引数として用いる。SchemaValidation プログラムにとっての
#   第一引数の値は "resource:/schema/stream-status.yaml" で固定。
#
exec mvn -q exec:java \
 -Dexec.mainClass=com.example.SchemaValidation \
 -Dexec.args="resource:/schema/stream-status.yaml ${1}"

これまでに用意したファイル群を次のように配置します。

.
├── input
│   ├── status_enabled.json
│   ├── status_missing.json
│   └── status_unknown.json
├── pom.xml
├── run.sh
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── SchemaValidation.java
        └── resources
            └── schema
                └── stream-status.yaml

最後にプログラムをコンパイルして準備完了です。

mvn compile

バリデーション動作確認

それでは動作確認をしていきましょう。

まず、正しいデータを含む status_enabled.json を入力とし、プログラムを実行してみます。

正しいデータを入力とし、プログラムを実行する
./run.sh input/status_enabled.json

結果は次のように表示されます。ここで true は、入力データがスキーマ定義に従っていることを示しています。

入力データが正しいという結果
validation result = true

次に、必須パラメータである status が含まれていないデータ status_missing.json を入力とし、プログラムを実行してみます。

必須パラメータの status が含まれていないデータを入力とし、プログラムを実行する
./run.sh input/status_missing.json

結果は次のように表示されます。エラーメッセージは status が含まれていないこと示しており、バリデーションの結果は false (入力データがスキーマ定義に従っていない) となっています。

必須プロパティ status が含まれていないことを示すエラー
: required property 'status' not found
validation result = false

最後に、status パラメータの値が無効であるデータ status_unknown.json を入力とし、プログラムを実行してみます。

status パラメータの値が無効であるデータを入力とし、プログラムを実行する
./run.sh input/status_unknown.json

結果は次のように表示されます。エラーメッセージは status の値が enabledpauseddisabled のいずれでもないことを示しています。

status パラメータの値が無効であることを示すエラー
/status: does not have a value in the enumeration ["enabled", "paused", "disabled"]
validation result = false

おわりに

苦い経験を経て、「Web フレームワークに任せず、入力データのバリデーションおよび POJO オブジェクトへの変換はアプリケーションレイヤーでやったほうがよい」と考えるようになりました。スキーマ定義によるバリデーションは、そんな私にとって大きな助けとなっています。また、OpenAPI ドキュメントと実装が自動的に同期するようにもなり、一石二鳥です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?