はじめに (対象読者・この記事でわかること)

この記事は、Spring BootでREST APIを開発しているが「バリデーションエラーのメッセージをフィールドごとに出し分けたい」「メッセージの変更がソースにハードコードされていて保守が大変」という悩みを持つ中級Java開発者を対象にしています。
記事を読むことで、Javaの独自アノテーション(カスタムアノテーション)を作成し、ConstraintValidatorとMessageSourceを連携させることで、1行のメタデータ変更だけでエラーメッセージを切り替えられるようになります。さらに、YAMLやプロパティファイルからメッセージを一元管理する方法も習得でき、メッセージ変更による再ビルド・再デプロイの手間をゼロにできます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Java 11以上の基本的な文法(アノテーションの定義方法) - Spring Boot 3.xでのRESTコントローラ作成経験 - Bean Validation(javax.validation/jakarta.validation)の基礎知識

なぜ独自アノテーションなのか? ~メッセージ出し分けの壁~

Spring Bootで@NotBlank@Sizeを使うと、デフォルトメッセージが英語のまま返ってきて「日本語にしたい」「フィールドごとにメッセージを変えたい」と思ったことはありませんか?
application.ymlspring.messages.basename=messagesを書いても、メッセージキーはjavax.validation.constraints.NotBlank.messageのように固定されてしまい、フィールド単位で出し分けできません。
そこで「フィールド名+ルール」をキーにしてメッセージを引ける独自アノテーションを作ることで、
・メッセージファイルだけで完結
・ソース修正なしで文言変更
・多言語対応も自然にサポート
という3つのメリットを同時に得られます。

カスタムアノテーションでエラーメッセージを自在に操る

ステップ1:アノテーション定義@PasswordValid

まず、対象フィールドに付与するアノテーションを定義します。@PasswordValidという名前で、属性値messageKeyを持たせて、メッセージキーを明示的に指定できるようにします。

Java
@Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PasswordValidator.class) public @interface PasswordValid { String messageKey() default "password.invalid"; // デフォルトキー Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }

ステップ2:ConstraintValidator実装@PasswordValidator

次に、実際の検証ロジックを担当するPasswordValidatorを実装します。ここでMessageSourceをインジェクションして、メッセージキーから文言を解決します。

Java
public class PasswordValidator implements ConstraintValidator<PasswordValid, String> { @Autowired private MessageSource messageSource; private String messageKey; @Override public void initialize(PasswordValid annotation) { this.messageKey = annotation.messageKey(); } @Override public boolean isValid(String value, ConstraintValidatorContext ctx) { if (value == null || value.length() < 8) { ctx.disableDefaultConstraintViolation(); String msg = messageSource.getMessage(messageKey, null, Locale.getDefault()); ctx.buildConstraintViolationWithTemplate(msg).addConstraintViolation(); return false; } return true; } }

ステップ3:メッセージファイル(messages.yml)を用意

src/main/resources/messages.ymlにフィールドごとのキーを定義します。

Yaml
password.invalid: "パスワードは8文字以上で入力してください" password.admin.invalid: "管理ユーザーのパスワードは12文字以上で入力してください"

ステップ4:DTOで使ってみる

Java
public class UserRequest { @PasswordValid(messageKey = "password.admin.invalid") private String adminPassword; @PasswordValid // デフォルトキーが適用される private String userPassword; }

ハマった点:MessageSourceがnullになる

Bean ValidationはデフォルトでSpringのDIコンテナ外で動作するため、@AutowiredしたMessageSourceが注入されずにNullPointerExceptionが発生します。

解決策:LocalValidatorFactoryBeanを明示的に定義

@Configurationクラスで以下のようにBeanを定義することで、Springのコンテナを使ったValidatorを強制します。

Java
@Configuration public class ValidationConfig { @Bean public LocalValidatorFactoryBean validator(MessageSource messageSource) { LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean(); bean.setValidationMessageSource(messageSource); return bean; } }

これでPasswordValidator内でmessageSource.getMessage()が正常に動作し、フィールドごとに異なるメッセージを返せるようになります。

まとめ

本記事では、Java独自アノテーションを使ってBean Validationのエラーメッセージをフィールド単位で出し分ける方法を解説しました。

  • カスタムアノテーションを定義しmessageKey属性を持たせるだけでYAML/プロパティファイルからメッセージを引ける
  • LocalValidatorFactoryBeanを明示的に定義することでSpringのMessageSourceと連携できる
  • ソースを修正せずにメッセージファイルだけで文言変更・多言語対応が可能に

この記事を通して、メッセージ変更による再ビルド・再デプロイの手間をゼロにし、保守性を大幅に向上させられました。
次回は、同手法を@EmailValid@PhoneValidなどに拡張し、共通ライブラリとして社内で共有する方法を紹介します。

参考資料