検証エラーをまとめて返す
このレッスンでは、検証エラーを1件で打ち切らずにまとめて返す設計と、その受け皿である BindingResult を理解します。
1件ずつ返す API は使いにくいものです
入力フォームに5つの項目があり、そのうち3つが誤っているとします。サーバーが最初の1件だけを返すと、利用者は直して送信し、また次のエラーを見せられ、また直す、という往復を3回することになります。まとめて返せば1回で済みます。エラーを集約するかどうかは、単なる実装の都合ではなく利用者の体験そのものです。
Spring はすべての制約を評価します
Bean Validation の Validator は、最初の違反で止まりません。対象オブジェクトのすべての制約を評価し、違反の集合を返します。@Valid を付けた引数の直後に BindingResult 型の引数を置くと、その集合を自分で受け取れます。
Java
@PostMapping("/users")
public ResponseEntity<?> create(@Valid @RequestBody UserRequest req,
BindingResult result) {
if (result.hasErrors()) {
List<String> messages = result.getFieldErrors().stream()
.map(e -> e.getField() + "=" + e.getDefaultMessage())
.toList();
return ResponseEntity.badRequest().body(messages);
}
// 正常系
}ここで重要なのは、BindingResult を引数に置いた瞬間、Spring は例外を投げなくなるという点です。検証に失敗しても制御はメソッドの中へ入り、続きは自分で書くことになります。逆に BindingResult を置かなければ MethodArgumentNotValidException が飛び、既定のエラー応答になります。どちらを選ぶかで責任の所在が変わります。
| 書き方 | 違反時の振る舞い |
|---|---|
@Valid だけ | 例外が投げられ、既定の 400 応答になる |
@Valid と BindingResult | メソッドに入り、自分で応答を組み立てる |
どのフィールドが悪かったのかを返します
エラーの本文で最も価値のある情報は「どの項目が」「なぜ」駄目だったのかです。FieldError は getField() でフィールド名、getDefaultMessage() でメッセージ、getRejectedValue() で拒否された値を返します。これを使うと、次のような応答が組めます。
JSON
{"errors":[{"field":"name","reason":"required"},{"field":"age","reason":"range"}]}クライアントはこの field を見て、対応する入力欄の横にメッセージを出せます。逆に文章を1本返すだけの設計では、どの欄が悪いのかを画面側で推測することになり、うまくいきません。
順序を決めておきます
Validator が返す違反の集合には、決まった順序がありません。応答の順が呼び出しごとに変わるとテストが安定しないので、フィールドの定義順やアルファベット順など、自分で並びを決めて整えるのが実務的です。
演習で書くもの
演習では、複数の検証を全部評価して結果を集め、フィールド名の付いた形で連結して返します。区切りにはイコールとカンマを使い、name=required,age=range のような文字列を組み立てます。エラーが1件も無ければ ok を返します。Spring では BindingResult から取り出して整形する部分にあたる処理を、検証は用意済みの補助メソッドに任せ、ここでは結果の集約を自分で書きます。前のレッスンとの違いは、最初の1件で return せず、最後まで集める点です。
演習で使うひな形
ひな形には空の List<String> があります。add は末尾へ1件追加し、String.join は追加順に指定の区切り文字でつなぎます。
Java
errors.add("name=required");
errors.add("age=range");
String result = String.join(",", errors); // "name=required,age=range"空の List を連結すると空文字です。検証は補助メソッドにあり、例えば ageError(200) は "age=range"、ageError(20) は null です。正常の null を除き、全項目のエラーを順に集めます。
要件
- 3つの補助メソッドで全項目を検証すること
- null でない結果だけを name・age・email の順に errors.add で追加すること
- String.join でカンマ区切りにし、エラーが無ければ ok を返すこと