はじめに
公開している Web API 仕様と実際の実装に差が出ないようにするにはどうすればいいのでしょうか? いろいろ考えた結果、現在開発中の新製品では次の方法を取ることにしました。
| 手順 | |
|---|---|
| 1 | Web API 仕様を OpenAPI で記述する |
| 2 | その OpenAPI ドキュメントの内容を MicroProfile OpenAPI 仕様に従って /openapi エンドポイントで公開する |
| 3 | Web API の実装では、その OpenAPI ドキュメントを用いてリクエストボディのバリデーションを行う |
この記事では、上記の最後の手順に相当する、「スキーマ定義ファイルを用いて JSON バリデーションを行う」方法について簡易実装を紹介します。
実装から OpenAPI ドキュメントを生成する方法があることは知っていますが、仕組み上、満足できるドキュメントが生成されそうもなかったので、その方法は取らないことにしました。
スキーマ定義を用いたバリデーションの実装
ここで紹介する実装では json-schema-validator ライブラリを用います。pom.xml に com.networknt:json-schema-validator を追加することで当ライブラリを利用できます。
<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
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
<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
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 | 更新後のステータス 有効な値は enabled、paused、disabled
|
reason |
任意 | string | ステータス更新の理由 |
そして、テスト用の入力ファイルを三つ用意します。
status_enabled.json — 正しいデータ
{
"stream_id": "stream_id_1",
"status": "enabled"
}
status_missing.json — 必須パラメータの status を含んでいないデータ
{
"stream_id": "stream_id_1"
}
status_unknown.json — status パラメータの値が無効のデータ
{
"stream_id": "stream_id_1",
"status": "unknown"
}
また、テストプログラム SchemaValidation を起動するためのシェルスクリプト 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 を入力とし、プログラムを実行してみます。
./run.sh input/status_missing.json
結果は次のように表示されます。エラーメッセージは status が含まれていないこと示しており、バリデーションの結果は false (入力データがスキーマ定義に従っていない) となっています。
: required property 'status' not found
validation result = false
最後に、status パラメータの値が無効であるデータ status_unknown.json を入力とし、プログラムを実行してみます。
./run.sh input/status_unknown.json
結果は次のように表示されます。エラーメッセージは status の値が enabled、paused、disabled のいずれでもないことを示しています。
/status: does not have a value in the enumeration ["enabled", "paused", "disabled"]
validation result = false
おわりに
苦い経験を経て、「Web フレームワークに任せず、入力データのバリデーションおよび POJO オブジェクトへの変換はアプリケーションレイヤーでやったほうがよい」と考えるようになりました。スキーマ定義によるバリデーションは、そんな私にとって大きな助けとなっています。また、OpenAPI ドキュメントと実装が自動的に同期するようにもなり、一石二鳥です。