Spring Bootのバリデーション入門|@Valid / BindingResultの使い方を解説

Spring BootでAPIやフォームを作り始めると、意外と早い段階で悩むのが入力チェックです。

必須項目が空だったり、文字数が長すぎたり、形式が違っていたり。
こうした判定をその都度if文で書いていくこともできますが、項目が増えるほどControllerが見づらくなっていきます。

しかも、入力チェックがあちこちに散らばると、例外処理との役割分担も曖昧になりがちです。
「どこで検証して、どこでエラーを返すのか」がぼやけると、コード全体の見通しも悪くなります。

Spring Bootでは、Jakarta Bean ValidationとSpring MVCの仕組みを使うことで、入力値のチェックを宣言的に書けます。
この記事では、Spring Bootでバリデーションを実装する基本の流れを、入門向けに整理します。

目次

Spring Bootのバリデーションでできること

ここでは、Spring Bootでバリデーションを使うと何がしやすくなるのかを整理します。

バリデーションが必要になる場面

バリデーションは、入力値が期待どおりかどうかを確認するための仕組みです。

たとえば、会員登録APIなら次のようなチェックが必要になります。

  • 名前が空ではないか
  • メールアドレスの形式が正しいか
  • パスワードの文字数が短すぎないか
  • 年齢が許可範囲に入っているか

こうしたルールをDTOにまとめておくと、Controller側で細かい判定を何度も書かずに済みます。
どの項目にどんな条件があるのかも見やすくなるので、修正や追加にも対応しやすくなります。

Spring Bootで使う主な仕組み

Spring Bootのバリデーションで、まず押さえたいのは次の3つです。

  • 制約アノテーション
  • @Valid / @Validated
  • BindingResult

制約アノテーションは、@NotBlank@Size のように、項目ごとのルールを表すものです。
@Valid は、そのDTOを検証対象にするために使います。
BindingResult は、バリデーションエラーの内容を受け取るときに使います。

最初はこの3つの役割だけ分かれば十分です。
細かい仕組みまで全部追わなくても、基本的な入力チェックは組めるようになります。

@Valid と @Validated の違い

ここは最初につまずきやすいところですが、入門段階ではあまり複雑に考えなくて大丈夫です。

まずは、基本的なDTOの検証なら @Valid を使う と覚えるのが分かりやすいです。
@Validated はSpring側の拡張も含んだアノテーションで、グループ指定などが必要な場面で使われます。

ただ、最初のうちは @Valid が使えれば困ることはあまりありません。
今回は入門記事なので、実装例も @Valid を中心に進めます。

まず押さえたい基本アノテーション

ここでは、実務でもよく使う制約アノテーションを絞って見ていきます。

@NotNull / @NotEmpty / @NotBlank の違い

この3つは似ていますが、意味は同じではありません。

  • @NotNull
    nullを禁止します
  • @NotEmpty
    nullと空文字、空コレクションを禁止します
  • @NotBlank
    null、空文字、空白だけの文字列を禁止します

文字列の必須入力では、@NotBlank を使うことが多いです。
たとえば名前やタイトルのように、スペースだけの入力も無効にしたい場合は、@NotBlank のほうが実務に合いやすくなります。

逆に、単純にnullだけ防ぎたい項目なら @NotNull で十分です。
同じ必須チェックでも、何を禁止したいのかを意識して選ぶことが大事です。

@Size / @Min / @Max / @Pattern の使いどころ

よく使うのは次のようなアノテーションです。

  • @Size
    文字列やコレクションの長さを制限したいとき
  • @Min / @Max
    数値の範囲を制限したいとき
  • @Pattern
    正規表現で形式を制限したいとき

たとえば、パスワードの文字数には @Size、年齢の範囲には @Min / @Max が向いています。
郵便番号や社員番号のように、入力形式をある程度固定したいなら @Pattern も候補になります。

ただし、何でも正規表現で厳しく縛ればいいわけではありません。
条件が複雑すぎるとDTOが読みにくくなるので、まずは基本的な入力ルールだけを素直に書くのがおすすめです。

メッセージ指定の基本

制約アノテーションには、エラーメッセージを指定できます。

@NotBlank(message = "名前は必須です")
@Size(max = 50, message = "名前は50文字以内で入力してください")
private String name;

入門段階では、何が問題なのかがすぐ分かるメッセージ にしておけば十分です。
最初から共通メッセージ化や多言語対応まで広げると、記事の主題がぶれやすくなります。

まずは、入力した人やAPI利用者が見て困らない文言になっているかを意識すると、自然な設計になりやすいです。

@Valid を使った基本実装

ここでは、もっとも基本的なバリデーション実装を確認します。

リクエストDTOに制約を付ける

まずはリクエストDTOに制約を書きます。

package com.example.demo.user;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class CreateUserRequest {

    @NotBlank(message = "名前は必須です")
    @Size(max = 50, message = "名前は50文字以内で入力してください")
    private String name;

    @NotBlank(message = "メールアドレスは必須です")
    @Email(message = "メールアドレスの形式が正しくありません")
    private String email;

    @NotBlank(message = "パスワードは必須です")
    @Size(min = 8, max = 100, message = "パスワードは8文字以上100文字以内で入力してください")
    private String password;

    @NotNull(message = "年齢は必須です")
    @Min(value = 0, message = "年齢は0以上で入力してください")
    @Max(value = 120, message = "年齢は120以下で入力してください")
    private Integer age;

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public String getPassword() {
        return password;
    }

    public Integer getAge() {
        return age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

ここで大事なのは、入力ルールをDTOに集約していること です。
Controllerにif文を並べるより、何をチェックしているのかがかなり見やすくなります。

また、DTOに書いておくと、入力仕様の変更にも追いやすくなります。
実務でも、まずはこの形から始めることが多いです。

Controllerで @Valid を付ける

次に、Controllerの引数に @Valid を付けます。

package com.example.demo.user;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping
    public ResponseEntity<String> createUser(@Valid @RequestBody CreateUserRequest request) {
        // この時点で、バリデーションを通過した入力だけを扱う前提で処理できる
        return ResponseEntity.ok("user created");
    }
}

これで、リクエストを受け取る時点でDTOの制約が評価されます。
不正な入力があれば、Controller本体の処理へ進む前にエラーになります。

この形のよいところは、保存処理や業務ロジックに入る前に、最低限の入力不備を弾けることです。
まずはこの流れを作れるようになるだけでも、コードの見通しはかなりよくなります。

エラー時にどうなるかを確認する

たとえば、name が空文字だったり、password が短すぎたりすると、バリデーションエラーになります。

ここで意識したいのは、バリデーションの役割は入力の不正を早い段階で止めることだという点です。
「エラーをどんなJSONで返すか」まで考え始めると、話が例外処理の設計に寄っていきます。

そのため、この記事ではまず 入力チェックの入口を整えること に集中するのがちょうどよいです。
エラーレスポンスの整え方は、例外処理の記事と分けて考えるほうが整理しやすくなります。

BindingResult の使い方

ここでは、バリデーションエラーをController内で扱いたい場合の基本を見ます。

BindingResult を使う場面

BindingResult は、エラー情報をその場で受け取りたいときに使います。

たとえば、フォーム入力でエラー内容を画面に戻したい場合や、Controller内で簡単にメッセージ一覧を作りたい場合です。
REST APIでも使えますが、APIでは例外処理でまとめて返す設計のほうがすっきりすることも多いです。

つまり、BindingResult は便利ですが、常に使うものではありません。
その場で個別にエラーを扱いたいときに使う くらいの理解で十分です。

エラー情報の取り出し方

BindingResult を使うと、どの項目で何が問題だったのかを取り出せます。

package com.example.demo.user;

import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping("/with-binding-result")
    public ResponseEntity<?> createUserWithBindingResult(
            @Valid @RequestBody CreateUserRequest request,
            BindingResult bindingResult) {

        if (bindingResult.hasErrors()) {
            List<String> messages = bindingResult.getFieldErrors().stream()
                    .map(error -> error.getField() + ": " + error.getDefaultMessage())
                    .toList();

            return ResponseEntity.badRequest().body(messages);
        }

        return ResponseEntity.ok("user created");
    }
}

ここでは、getFieldErrors() を使ってフィールド単位のエラーを取り出しています。
どの項目で失敗したのかがすぐ分かるので、まずはこの形から覚えるのが分かりやすいです。

複雑なレスポンス形式に整えることもできますが、最初からそこまで作り込まなくても大丈夫です。
まずは、エラー内容を取得できる ことを押さえれば十分です。

どこまでControllerで扱うべきか

BindingResult を使うと柔軟に制御できますが、毎回Controllerで個別対応し始めると、同じようなコードが増えやすくなります。

そのため、使い分けは次のように考えると整理しやすいです。

  • 画面向けで、その場の再表示が必要な場合
    BindingResult を使う
  • APIで共通のエラーレスポンスに寄せたい場合
    例外処理でまとめる

入門の段階では、どちらが絶対に正しいというより、やり方が複数ある ことを知っておくのが大切です。
そのうえで、今のプロジェクトに合う形を選ぶと迷いにくくなります。

実務で迷いやすいポイント

ここでは、コードが動くだけでは判断しづらい部分を整理します。

DTOに付けるべきか Entityに付けるべきか

まず結論からいうと、Web入力のバリデーションはDTOに付けるほうが分かりやすい ことが多いです。

理由は、入力条件がユースケースごとに変わるからです。
たとえば新規登録では必須でも、更新では任意という項目は珍しくありません。

これをEntityにそのまま書いてしまうと、登録用、更新用、管理画面用の条件が混ざっていきます。
その結果、どの入力条件を前提にしているのかが分かりにくくなります。

もちろん、ドメインとして絶対に守るべき制約には別の考え方もあります。
ただ、入門としてはまず 入力チェックはDTOに書く と整理しておくのが分かりやすいです。

バリデーションと例外処理の役割分担

この2つは一緒に語られやすいですが、役割は別です。

  • バリデーション
    入力値がルールを満たしているかを確認する
  • 例外処理
    エラーが起きたときに、どう返すかを整理する

ここが混ざると、「DTOで何を見るのか」「ControllerAdviceで何を返すのか」が曖昧になります。
結果として、同じエラー処理があちこちに散らばりやすくなります。

今回の記事では、あくまで 入力をどう検証するか を中心にしています。
エラーレスポンスの統一や設計方針までは、別記事と分けて考えるほうが理解しやすいです。

やりすぎなバリデーションを避ける考え方

バリデーションは便利ですが、詰め込みすぎると逆に読みにくくなります。

たとえば、DTOに長い正規表現や複雑な業務条件を大量に書き始めると、「この項目は結局何を見ているのか」が分かりにくくなります。
コードとしては書けても、保守しやすいとは限りません。

まずは、次のくらいで分けて考えるとバランスが取りやすいです。

  • 必須チェック
  • 文字数制限
  • 数値範囲
  • 基本的な形式チェック

このあたりはバリデーションで扱いやすいです。
一方で、複雑な業務判断まで全部DTOに背負わせるのは避けたほうが、後から見たときに整理しやすくなります。

よくあるつまずき

ここでは、実際によくハマるポイントを確認します。

@Valid を付けたのに動かない

よくある原因のひとつは、バリデーション用の依存関係が入っていないことです。

Spring Bootでは、通常 spring-boot-starter-validation を追加して使います。
Spring Initializrで作成したプロジェクトでは最初から入っていることもありますが、手動で依存関係を調整している場合は確認が必要です。

Mavenなら次のように追加します。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Gradleならこちらです。

implementation 'org.springframework.boot:spring-boot-starter-validation'

「コードは合っていそうなのに反応しない」というときは、まずここを確認すると切り分けしやすくなります。

6-2. BindingResult の位置が違う

BindingResult は、対応する検証対象の直後に置く必要があります。

正しい例は次の形です。

public ResponseEntity<?> createUser(
        @Valid @RequestBody CreateUserRequest request,
        BindingResult bindingResult) {
    // ...
}

間に別の引数を挟むと、意図した形で扱えないことがあります。
見落としやすいですが、かなり初歩的によくあるつまずきです。

ネストしたオブジェクトが検証されない

親DTOの中に子DTOを持っている場合、子側まで検証したいならネスト先にも @Valid が必要です。

package com.example.demo.user;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;

public class CreateUserRequest {

    @NotBlank(message = "名前は必須です")
    private String name;

    @Valid
    private AddressRequest address;

    public String getName() {
        return name;
    }

    public AddressRequest getAddress() {
        return address;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAddress(AddressRequest address) {
        this.address = address;
    }

    public static class AddressRequest {

        @NotBlank(message = "都道府県は必須です")
        private String prefecture;

        public String getPrefecture() {
            return prefecture;
        }

        public void setPrefecture(String prefecture) {
            this.prefecture = prefecture;
        }
    }
}

ここを忘れると、親には @Valid を付けているのに、子オブジェクトの制約がチェックされません。
「付けたはずなのに動かない」と感じたときは、このパターンも疑うとよいです。

まとめ

Spring Bootのバリデーションは、最初は難しそうに見えても、やること自体はそこまで多くありません。

まずは、DTOに制約アノテーションを書くこと。
次に、Controllerで @Valid を付けること。
必要に応じて BindingResult でエラーを受け取ること。

この流れが押さえられれば、入門としては十分です。

特に、入力チェックの責務をControllerのif文から切り離せる ようになると、コードの見通しはかなりよくなります。
そのうえで、エラーの返し方まで整えたくなったら、例外処理の記事へつなげると理解しやすいです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次