概要

フォーム入力や CSV 取込でデータを受け取るとき、最初に必要になるのがバリデーションです。「数値として解釈できるか」「メールアドレスの形式は正しいか」「日付として存在するか」――個々のチェックは単純でも、複数フィールドのエラーをまとめて返す、ルールを宣言的に管理する、バージョンごとの書き方の違いに対応する、といった実務上の要件を加えると途端に設計判断が増えます。この記事では、Java 標準 API の正規表現と例外制御を使い、メール・電話番号・年齢・日付のバリデーションを実装します。Java 17 では record でバリデーション結果を型付けし、Java 21 では sealed interface と switch 式でルールを宣言的に管理する書き方も紹介します。Bean Validation(Jakarta Validation)に頼らず、ロジックの中身を自分で把握できる形を目指します。

使いどころ

社内システムのユーザー登録画面で、名前・メール・年齢・生年月日を一括バリデーションし、全エラーをまとめて画面に返す

CSV ファイル取込時に各行のフィールドを検証し、不正な行をエラーリストとして出力する

API リクエストのパラメータを受け取った時点で形式チェックを行い、不正値は 400 Bad Request として即座に返却する

コード例

InputValidator.java
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

public class InputValidator {

    // バリデーション結果(Java 16+ record)
    record ValidationResult(boolean valid, List<String> errors) {
        static ValidationResult of(List<String> errors) {
            return new ValidationResult(errors.isEmpty(), List.copyOf(errors));
        }
    }

    // メールアドレス形式
    private static final Pattern EMAIL_PATTERN =
        Pattern.compile("^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$");

    // 日本の電話番号形式
    private static final Pattern PHONE_PATTERN =
        Pattern.compile("^(0[0-9]{1,4}-[0-9]{1,4}-[0-9]{4}|0[0-9]{9,10})$");

    /** 文字列が整数として解釈できるかを判定 */
    public static boolean isInteger(String value) {
        if (value == null || value.isBlank()) return false;
        try {
            Integer.parseInt(value);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    /** メールアドレスの形式チェック */
    public static boolean isValidEmail(String email) {
        if (email == null || email.isBlank()) return false;
        return EMAIL_PATTERN.matcher(email).matches();
    }

    /** 電話番号の形式チェック(日本) */
    public static boolean isValidPhone(String phone) {
        if (phone == null || phone.isBlank()) return false;
        return PHONE_PATTERN.matcher(phone).matches();
    }

    /** 日付の形式チェック(ISO yyyy-MM-dd) */
    public static boolean isValidDate(String dateStr) {
        if (dateStr == null || dateStr.isBlank()) return false;
        try {
            LocalDate.parse(dateStr);
            return true;
        } catch (DateTimeParseException e) {
            return false;
        }
    }

    /** 年齢のバリデーション(必須 + 数値 + 範囲) */
    public static List<String> validateAge(String value) {
        var errors = new ArrayList<String>();
        if (value == null || value.isBlank()) {
            errors.add("年齢は必須です");
            return errors;
        }
        try {
            int age = Integer.parseInt(value);
            if (age < 0)   errors.add("年齢は0以上を入力してください");
            if (age > 150)  errors.add("年齢は150以下を入力してください");
        } catch (NumberFormatException e) {
            errors.add("年齢は数値で入力してください");
        }
        return errors;
    }

    /** 複数フィールドをまとめてバリデーション */
    public static ValidationResult validateUserInput(
            String name, String email, String ageStr, String birthDate) {
        var errors = new ArrayList<String>();

        if (name == null || name.isBlank()) {
            errors.add("名前は必須です");
        } else if (name.length() > 50) {
            errors.add("名前は50文字以内で入力してください");
        }

        if (!isValidEmail(email)) {
            errors.add("メールアドレスの形式が正しくありません");
        }

        errors.addAll(validateAge(ageStr));

        if (!isValidDate(birthDate)) {
            errors.add("生年月日は yyyy-MM-dd 形式で入力してください");
        }

        return ValidationResult.of(errors);
    }

    public static void main(String[] args) {
        // エラーケース
        var result = validateUserInput("", "invalid", "abc", "2024-13-01");
        System.out.println("エラー " + result.errors().size() + " 件:");
        result.errors().forEach(e -> System.out.println("  - " + e));

        // 正常ケース
        var ok = validateUserInput("山田太郎", "[email protected]", "30", "1994-05-20");
        System.out.println("valid: " + ok.valid());
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record で ValidationResult を定義し、エラーリストと valid フラグを型で表現できる。isBlank() や var による型推論で記述量が減る。

Java 17
// Java 17: record でバリデーション結果を型付け
record ValidationResult(boolean valid, List<String> errors) {
    static ValidationResult of(List<String> errors) {
        return new ValidationResult(errors.isEmpty(),
                List.copyOf(errors));
    }
}
// isBlank() でスペースのみも検出
if (name == null || name.isBlank()) { ... }

Library Comparison

標準 API(Pattern + 例外制御)バリデーション対象が限定的で、ルールの中身を自分で把握・制御したいとき。依存ゼロで動作する。フィールド数やルール数が増えると、手続き的な記述が冗長になりやすい。
Jakarta Bean Validation(Hibernate Validator)アノテーションベースでエンティティのフィールドにルールを宣言的に付与したいとき。Spring との統合が前提の場合。依存が増え、カスタムバリデータの実装にはアノテーションプロセッサの知識が必要になる。ルールのデバッグもアノテーション経由になり見通しが下がる場合がある。
Apache Commons Validatorメール・URL・クレジットカード番号など汎用的なフォーマットチェックを手早く済ませたいとき。日本固有のフォーマット(電話番号・郵便番号)は自前実装が必要。依存に見合うかはプロジェクト規模による。

注意点

Integer.parseInt は NumberFormatException を投げるが、Long や BigDecimal への変換が必要なケースも業務では多い。数値バリデーションの対象型は仕様から確認すること

メールアドレスの正規表現は RFC 5322 を完全にカバーするものではない。厳密な検証が必要な場合は InternetAddress.validate() を使うか、実際にメールを送って到達確認するのが確実

isBlank() は Java 11 以降のメソッド。Java 8 では trim().isEmpty() で代替する必要がある

LocalDate.parse は存在しない日付(2月30日など)を DateTimeParseException で弾くが、ロケール依存のフォーマット(和暦・スラッシュ区切り)は別途 DateTimeFormatter を用意する必要がある

バリデーションエラーは最初の1件で止めずに、全件を集約して返すのが UX 上望ましい。短絡評価で実装しないよう注意する

FAQ

バリデーションエラーは最初の1件で返すべきですか、全件まとめて返すべきですか。

画面入力やAPI応答では全件まとめて返すのが一般的です。ユーザーが1件ずつ修正して再送信する手間を減らせます。ただし、前提条件の崩壊(認証失敗など)は即座に返すべきです。

メールアドレスの正規表現はどこまで厳密にすべきですか。

RFC 5322 を完全にカバーする正規表現は非常に複雑で保守しにくくなります。一般的な形式チェック(@の存在とドメイン部の構造)で十分なケースが多く、最終的にはメール送信による到達確認が確実です。

Bean Validation と自前実装のどちらを選ぶべきですか。

Spring Boot や Jakarta EE を使う場合は Bean Validation が自然な選択です。フレームワークに依存しない共通ライブラリや、バリデーションロジックを業務層に閉じたい場合は自前実装のほうが見通しがよくなります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます