0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Jakarta Validation】メッセージ生成は validate のあとで

0
Last updated at Posted at 2026-03-26

結論

Jakarta Validation では validate と同時にメッセージが生成されますが、 ExceptionHandler 側でメッセージ生成する方が設計上すっきりするよね、というお話と、その機能を持つ ecuacion-lib-validation の紹介です。

はじめに

Jakarta Validation は、チェックロジックを annotation で宣言的に書けてとても便利。
ただ、メッセージまわりの設計で悩むことがちょいちょいあります。

その一つが「validate のタイミングでメッセージが生成されてしまう」問題。
今回は、これがなぜ問題になるのか、そしてどう解決するのかを書いてみます。

Jakarta Validation 標準のメッセージ生成タイミング

Jakarta Validation 標準では、validator.validate(...) の呼び出しのタイミングでメッセージが生成されます。

Main.java(一部抜粋)
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Account>> violations = validator.validate(account);

for (ConstraintViolation<Account> v : violations) {
    System.out.println(v.getMessage()); // ← すでに生成済みのメッセージが返ってくる
}

v.getMessage() は validate したタイミングで生成済みのメッセージを返すだけ、ということですね。

メッセージ生成に使用する locale は、デフォルトでは JVM のデフォルト locale が使用されます。

どこが問題なのか

ユーザごとに言語を切り替えるような、いわゆる多言語対応のシステムを考えてみます。

locale をユーザごとに変えたい場合、validator 生成時に locale の指定が必要です。

つまり、(作りにはよりますが)validate する場所(service 層など)で locale を取り扱う必要が発生します。。

AccountService.java(よくない例)
public void createAccount(Account account, Locale locale) { // ← locale を引数で受け取る羽目に
    ValidatorFactory factory = Validation.byDefaultProvider()
        .configure()
        .messageInterpolator(
            new LocaleSpecificMessageInterpolator(locale)) // ← locale をここで使用
        .buildValidatorFactory();

    Validator validator = factory.getValidator();
    Set<ConstraintViolation<Account>> violations = validator.validate(account);

    if (!violations.isEmpty()) {
        throw new ConstraintViolationException(violations);
    }
}

service 層のメソッドに Locale が引数として出てきており、これだと煩雑になりますよね。

validate とメッセージ生成を分離

解決策はシンプルで、「validate のタイミング」と「メッセージ生成のタイミング」を分離することです。

具体的には、以下の流れにします。

  1. service 層で validate(locale 不要)
  2. 検証エラーがあれば ConstraintViolationException を throw
  3. ExceptionHandler で catch
  4. ExceptionHandlerlocale を取得し、メッセージを生成

こうすることで、service 層は locale に一切関与しなくてよくなります。

AccountService.java(改善後)
public void createAccount(Account account) { // ← locale の引数が不要
    Set<ConstraintViolation<Account>> violations = validator.validate(account);

    if (!violations.isEmpty()) {
        throw new ConstraintViolationException(violations);
    }
}
GlobalExceptionHandler.java(イメージ)
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<String> handleConstraintViolation(
        ConstraintViolationException ex, Locale locale) {
    // ここで locale を使ってメッセージ生成
    List<String> messages = generateMessages(ex, locale);
    ...
}

validate した後に ConstraintViolationSetConstraintViolationException に包まれて飛んでくるので、ExceptionHandler でそれを受け取ってメッセージを生成する、というシンプルな設計です。

ecuacion-lib-validation : ExceptionUtil

この「ExceptionHandler 側でメッセージを生成する」を実現するのが、ecuacion-lib-validationExceptionUtil です。

ExceptionUtil.getMessageList は、ConstraintViolationException または Set<ConstraintViolation<?>> を受け取り、メッセージの List<String> を返します。

ecuacion-lib-validation の導入方法

導入方法については下記記事をご参照ください。

基本的な使い方

検証するためのオブジェクトが必要なので、こちらを使用します。

Account.java
public record Account(@NotNull String name) {

}

検証を実行するためのコードはこちら。

GlobalExceptionHandler.java
  private static void 基本的な使い方() {
    try {
      // 以下、service層のイメージ。ここではlocale不要
      Set<ConstraintViolation<Account>> violations = validator.validate(new Account(null));
      if (violations.size() > 0) {
        throw new ConstraintViolationException(violations);
      }

    } catch (ConstraintViolationException ex) {
      // 以下、exceptionHandler内のイメージ。ここでlocaleを使用
      for (String message : ExceptionUtil.getMessageList(ex, Locale.JAPANESE)) {
        System.out.println(message);
      }
    }
  }

実行した結果はこちら。

実行結果
入力必須です

ExceptionUtil.getMessageList を呼んだタイミングでメッセージが生成されます。validate したタイミングではありません。

ExceptionUtil のメッセージ内容

Jakarta Validation の標準機能(というか Hibernate Validator ですかね)だと通常、@NotNull だと null は許可されていません のようなメッセージになっています。

が、エンドユーザが読むには辛い(まず null がわからない^^;)ので、エンドユーザにもわかりやすいと思われるメッセージをデフォルトで保持しています。

もちろん、ValidationMessages.properties を定義することにより変更可能です。

サンプルコード

サンプルコードは以下です。
https://github.com/ecuacion-jp/ecuacion-code-snippets/tree/main/ecuacion-lib-core-JakartaValidationExceptionUtil

※このページから直接ソースの zip を download はできないと思うので、そのページにある ecuacion-code-snippets のリンクをクリックし、そこにある緑の <> Code ボタンから Download ZIP で download してください。

本サンプルのフォルダに移動後、mvn compile exec:java で実行できます。

まとめ

Jakarta Validation は validate とメッセージ生成が同一タイミングで行われる仕様ですが、ecuacion-lib-validationExceptionUtil を使って分離することで locale をロジック層に持ち込まなくて済む、というお話でした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?