このプロジェクトではSpring BootでRESTful APIを実装するときにバリデーションをどのように実装するかを検討します。
バリデーションとは、入力値が正しいかどうかをチェックすることです。バリデーションを行うことで、不正な入力値を排除することができます。
バリデーションは、データの信頼性、整合性、セキュリティを確保し、不正確なデータや悪意のあるデータがシステムに入り込むことを防ぐために非常に重要です。
バリデーションは、データが特定の条件に合致しているかどうかを確認し、データが整合性を持っているかどうかを検証します。
整合性のないデータは予測不能な動作やエラーを引き起こす可能性があります。
バリデーションは、セキュリティを向上させるために不正な入力データを検出します。
例えば、SQLインジェクションやクロスサイトスクリプティング(XSS)攻撃など、セキュリティ上のリスクを軽減します。
データバリデーションは、データ品質を向上させます。
データが正確で整った状態で保存され、処理されることで、意思決定や分析の信頼性が向上します。
バリデーションは、データがエラーを含んでいる場合に早期に検出し、適切なエラーメッセージやエラーハンドリングを提供します。
これにより、問題がシステムに深刻な影響を及ぼす前に対処できます。
バリデーションは、ビジネスルールをデータ層に実装するために使用されます。 たとえば、ユーザーの登録フォームにおいて、パスワードが一定の複雑さを持っている必要がある場合、その複雑さを確認するためのバリデーションルールを実装できます。
バリデーションは、ユーザーエクスペリエンスを向上させます。エラーメッセージやヒントを提供し、ユーザーに正しい入力を促すことができます。
バックエンドAPIでのバリデーションエラーが発生した場合、適切なエラーレスポンスをクライアントに返すことが重要です。
以下は、バックエンドAPIでのバリデーションエラー時に返すべき情報についての一般的なガイドラインです。
バリデーションエラーが発生した場合、HTTPステータスコードを400 Bad Requestとして返します。
これは、クライアントからのリクエストが不正確であることを示します。
エラーメッセージは、クライアントが理解しやすい形式で提供されるべきです。
JSON形式やXML形式など、APIがサポートするデータフォーマットに従ってエラーメッセージを構造化します。
エラーメッセージには、どの項目がバリデーションエラーを引き起こしたか、エラーの詳細な説明、および必要な場合は対処方法やヒントを含めることができます。
下記はエラーメッセージの例です。
{
"errors": [
{
"field": "name",
"message": "must not be blank"
}
]
}
エラーコードは、エラーを一意に識別するために使用されます。
APIを呼び出した側がエラーに対処するのに役立ちます。
セキュリティの観点から、バックエンドの例外情報を完全にクライアントに公開しないように注意してください。
エラー情報はクライアント向けに適切にマスキングまたは非表示にする必要があります。
HTTPステータスコードは400(Bad Request)を返します。
Spring Bootを使用してバリデーションを実装する際に、Bean ValidationとHibernate Validatorを組み合わせる方法を説明します。
Bean Validationは、Javaの標準仕様で、Hibernate ValidatorはBean Validationの実装の1つです。
これにより、簡単にバリデーションルールを定義し、実行できます。
Note
Java Bean Validationは、Javaの標準プラットフォーム仕様の一部として導入され、当初はjavax.validationパッケージで定義されていました。 しかし、Java EEがEclipse FoundationからJakarta EEに移行した後、Java EEから独立したプロジェクトとして、Jakarta Bean Validationとして再定義されました。 このため、Java EE 8以降では、javax.validationパッケージではなく、jakarta.validationパッケージを使用する必要があります。 バリデーション関連の記事ではjavax.validationパッケージを使用しているものが多いですが、Java EE 8以降を前提にしているSpring Boot 2.3以降では、jakarta.validationパッケージを使用する必要があります。
バリデーションルールを定義するために、Bean Validationではアノテーションを使用します。
@NotNullアノテーションは、フィールドがnullでないことを検証します。
@NotEmptyアノテーションは、フィールドがnullまたは空でないことを検証します。
@NotBlankアノテーションは、フィールドがnullまたは空でないことを検証します。
@Sizeアノテーションは、フィールドのサイズが指定された範囲内であることを検証します。
その他のバリデーションアノテーションについては、以下のドキュメントを参照してください。
https://jakarta.ee/specifications/bean-validation/3.0/apidocs/
https://jakarta.ee/specifications/bean-validation/3.0/jakarta-bean-validation-spec-3.0.html#builtinconstraints
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-defineconstraints-spec
Bean Validation 一覧 で調べるのもよいですね。
Spring Bootでバリデーションを実装する際の流れは、以下の通りです。
Spring InitializrまたはMaven/Gradleプロジェクトを作成し、Spring Bootプロジェクトをセットアップします。
バリデーションに必要な依存関係をpom.xmlまたはbuild.gradleに追加します。
例えば、spring-boot-starter-validationを追加して、Hibernate Validatorなどのバリデーションライブラリを利用できます。
リクエストのデータモデルを表すクラスを作成します。 このクラスにはバリデーションルールを追加します。
たとえば、下記のようなjsonリクエストを送信しユーザーの氏名を登録するAPIがあると考えます。
{
"givenName": "Taro",
"familyName": "Yamada"
}
givenNameとfamilyNameは必須項目とし、nullや空文字、スペースのみを許可しないようにバリデーションルールを追加します。 つまり、下記のような登録は認めないようにします。
{
"givenName": "",
"familyName": ""
}
{
"givenName": " ",
"familyName": " "
}
{
"givenName": null,
"familyName": null
}
jsonに対応した以下のようなクラスを作成します。
@NotBlankアノテーションを使用して、givenNameとfamilyNameがnullまたは空でないことを検証します。
/usersのPOSTリクエストを受け付けるためのコントローラを作成します。 リクエストボディに@Validアノテーションを使用して、バリデーションを実行します。
ちなみにGETリクエストのクエリ文字列に対するバリデーションも同様に実装できます。
Warning @RequestParamアノテーションを使用する場合はControllerクラスに@Validatedアノテーションを追加しなければならず、実装方法が異なります。 @ValidアノテーションはJakarta Bean Validationで定義されています。 一方、@Validatedは@Validを拡張したSpringの機能です。
curlコマンドでリクエストを送信します。
# 200 OKが返却される
curl -X POST -H "Content-Type: application/json" -d '{"givenName": "Taro", "familyName": "Yamada"}' http://localhost:8080/users -i
# 400 Bad Requestが返却される
curl -X POST -H "Content-Type: application/json" -d '{"givenName": "", "familyName": ""}' http://localhost:8080/users -i
Spring Bootはバリデーションエラーが発生した場合、自動で400 Bad Requestを返却します。
また、バリデーションエラーが発生した場合、コントローラのメソッドは実行されませんので、登録処理は実行されません。
ここまでの手順で動作確認のためにリクエストを送ると下記のようなログが出力されているはずです。
Resolved [org.springframework.web.bind.MethodArgumentNotValidException
このことから、Spring Bootはバリデーションエラーが発生した場合、MethodArgumentNotValidExceptionが発生することがわかります。 エラー時のレスポンスをカスタマイズするには、このMethodArgumentNotValidExceptionをハンドリングする必要があります。
@ExceptionHandlerアノテーションと@ControllerAdviceアノテーションを使用して、MethodArgumentNotValidExceptionをハンドリングします。
curlコマンドでリクエストを送信します。
# 400 Bad Requestが返却される
curl -X POST -H "Content-Type: application/json" -d '{"givenName": "", "familyName": ""}' http://localhost:8080/users -i
レスポンスは下記のようになるはずです。
HTTP/1.1 400
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 17 Sep 2023 07:02:39 GMT
Connection: close
{"status":"BAD_REQUEST","message":"validation error","errors":[{"field":"givenName","message":"空白は許可されていません"},{"field":"familyName","message":"空白は許可されていません"}]}
jqというツールを使ってレスポンスのjsonをフォーマットすると下記のようになります。
% curl -X POST -H "Content-Type: application/json" -d '{"givenName": "", "familyName": ""}' http: //localhost:8080/users | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 242 0 207 100 35 91 15 0: 00: 02 0: 00: 02 --: --: -- 106
{
"status": "BAD_REQUEST",
"message": "validation error",
"errors": [
{
"field": "familyName",
"message": "空白は許可されていません"
},
{
"field": "givenName",
"message": "空白は許可されていません"
}
]
}
エラーレスポンスをカスタマイズして、バリデーションエラーのメッセージを返却することができました。
MockMvcを使用したテストコードを実装します。
このようにテストコードを実装することで、バリデーションエラー時のレスポンスが正しいことを確認することができます。
ただ、すべてのパターンの入力値をテストしてしまうとUserControllerTestクラスが肥大化してしまいます。
そこで、パラメータのバリデーションはFormのクラスの単体試験としてテストすることにします。
@NotBlank以外にも様々なバリデーションアノテーションを利用した実装とテストのサンプルコードを用意しました。
@NotBlankではフィールド単項目のチェックしかできませんが、@AssertTrueを使うことで相関項目のチェックを実装することができます。
たとえば、givenNameとfamilyNameを更新するときに、どちらも空文字の場合はエラーとするような場合です。
こういった相関項目チェックは、@AssertTrueを使うことで実装することができます。
他にも相関項目チェックは下記のようなユースケースがあります。
- パスワードとパスワードの確認フィールドが一致していない場合にエラーを表示する。
- 購入者が未成年の場合に購入できない商品の場合にエラーを表示する。
- ホテルの予約時にチェックアウト日がチェックイン日以前か同じ日である場合にエラーを表示する。
すでに定義されているバリデーションアノテーションでは実装できないバリデーションを実装する場合は、カスタムバリデーションアノテーションを定義することで実装することができます。 カスタムバリデーションアノテーションを定義するには、下記の2つのクラスを作成する必要があります。
- バリデーションアノテーションの定義クラス
- バリデーションアノテーションのバリデータクラス
バリデーションアノテーションの定義クラスは、@Constraintアノテーションを付与して定義します。
@Targetアノテーションで、バリデーションアノテーションを付与できる対象を指定します。 @Retentionアノテーションで、バリデーションアノテーションの有効期間を指定します。 @Constraintアノテーションで、バリデーションアノテーションのバリデータクラスを指定します。
バリデーションアノテーションのバリデータクラスは、ConstraintValidatorインターフェースを実装して定義します。
ConstraintValidatorインターフェースのジェネリクスには、バリデーションアノテーションの定義クラスとバリデート対象の値の型を指定します。
public class カスタムバリデータのクラス名 implements ConstraintValidator<バリデーションアノテーションの定義クラス名, バリデート対象の値の型>
バリデーションアノテーションのバリデータクラスでは、isValidメソッドを実装します。
boolean isValid(バリデート対象の値の型 value,ConstraintValidatorContext context);
isValidメソッドの第1引数には、バリデーション対象のフィールドの値が渡されます。 isValidメソッドの第2引数には、バリデーションのコンテキストが渡されます。 バリデーションのコンテキストでは、バリデーションの結果を設定したり、バリデーションのメッセージを設定したりすることができます。
バリデーションアノテーションをカスタマイズしてできることとしては下記のようなものがあります。
- 登録したいTODOのステータスがTODOかIN_PROGRESSのいずれかであることをチェックする
- そのサービス独自のパスワードのルールを満たしているかチェックする
- 宿の予約人数が、部屋の定員を超えていないかチェックする
また、@AssertTrueアノテーションを使ったチェックの事例を、バリデーションアノテーションをカスタマイズして実装することもできます。
独自のアノテーションを作るだけでなくJakarta Bean Validationで定義されているアノテーションを組み合わせる方法もあります。
たとえば、Visaの申し込みを想定するときに、申込可能な年齢が必須であり18歳以上45歳未満であることをチェックする場合、@Minと@Maxを組み合わせて実装することができます。
@NotNull
@Min(18)
@Max(45)
private Integer age;
このようにすればたしかに実装はできますが、年齢のチェックを行うアノテーションを作成したほうが、コードの可読性が高くなります。
また、年齢に関するチェック処理を1つにまとめることで、年齢に関するチェック処理の変更が発生した場合に、変更箇所が1箇所になるため、変更の影響範囲を把握しやすくなります。
テストコードはこちら。
Spring Bootでは、Jakarta Bean Validationの実装としてHibernate Validatorが利用されています。
Hibernate Validatorは、Bean Validationの実装の1つであり、Bean Validationの仕様に準拠しています。
Jakarta Bean ValidationとHibernate Validatorの仕様を理解し、それらがどのようにSpring Bootで利用されているかを理解することで、Spring Bootでバリデーションを実装することができます。
https://beanvalidation.org/
Jakarta Bean Validationの公式サイトです。
こちらの記事のプレゼン資料がわかりやすい。
https://beanvalidation.org/news/2018/02/26/bean-validation-2-0-whats-in-it/
https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#preface
https://docs.spring.io/spring-framework/reference/core/validation/beanvalidation.html
Springの公式ドキュメントで簡単にバリデーションを実装する方法が説明されています。
サラッと重要なことが書いてあるので、読んでおくと良いと思います。