Edited at

Hibernate Validator(Bean Validation)のメッセージにアノテーション属性以外の値を渡す方法(埋め込む方法)

Bean Validationのエラーメッセージには、以下のように制約アノテーションに指定した属性値を埋め込むことができます。(なお、本エントリーで扱う内容は、Hibernate Validatorを使う前提になります)


制約アノテーションの指定例

@Size(min = 4, max = 16)

private String foo;


バリデーション実行例

// ...

Bean bean = new Bean();
bean.foo = "123";
Set<ConstraintViolation<Bean>> violations = vb.validate(bean);
// ...

上記のケースで生成されるエラーメッセージは、「size must be between 4 and 16」になります。


アノテーション属性値以外を埋め込みたいと思った背景は・・・

今携わっている案件で、入力チェック時に参照する基準値を外部設定(実際にはプロパティファイル)から取得する必要性に迫られました。Bean Validationを使わずロジックでチェックするという選択肢もありましたが、入力チェックはできるだけBean Validationの仕組みを使って行いたかったので・・・Bean Validationで実現する方法を調査しました。


具体的にはどんな制約アノテーションを作ったの?

実際に作成したものではありませんが、「指定日数以降の日付であること」をチェックするアノテーションを作成し、「指定日数」の部分をプロパティファイルから取得するような制約アノテーションを作成することにしました。


制約アノテーションの作成例

@DaysLater(days = "foo.daysLater", defaultValue = 5)

private LocalDate fooDate;


プロパティファイルの設定例

foo.daysLater=10



src/main/resources/ValidationMessages.propertiesの設定例

com.example.validation.DaysLater.message = must be {days} days later



チェックはできたがメッセージが残念・・・

制約違反した際のエラーメッセージは「must be 10 days later」としたいところですが・・・残念ながらデフォルトの動作では「must be foo.daysLater days later」となってしまいます。ま〜当然の結果なのですが・・・


Bean Validation仕様準拠で対応可能か?

ちゃんと調べていませんが・・・ざっと仕様書を見た感じだとBean Validationの仕様内で対応する方法はなさそうでした。ただ・・・メッセージを組み立てる際にEL式が使えるので・・・EL式を使いこなせば対応できるかもしれません。


Hibernate Validatorの拡張機能を使えば対応できる!

Hibernate Validationが提供している拡張機能を使うと、メッセージ定義から参照できる値を変更することができます。


private int value; // アノテーションの属性値(プロパティキー)からプロパティ値を格納

// ...

public boolean isValid(LocalDate targetValue, ConstraintValidatorContext context) {

boolean result = // チェック内容は省略...

    // チェックNGの場合はデフォルト動作を無効化し、任意の制約違反情報を生成する
if (!result) {
// Hibernate Validatorの拡張機能を使うため、Hibernate Validator提供のインタフェースに変換
HibernateConstraintValidatorContext hibernateContext =
context.unwrap(HibernateConstraintValidatorContext.class);

// デフォルトの制約違反情報の生成を無効化
hibernateContext.disableDefaultConstraintViolation();

// 任意の制約違反情報を生成
hibernateContext
// メッセージ定義で{value}で値を参照できるようにする
.addMessageParameter("days", this.days)
// メッセージ定義内のEL式でvalueという変数名で値を参照できるようにする
.addExpressionVariable("days", this.days)
// メッセージのテンプレートはアノテーションに指定しているテンプレートを使用する(デフォルト動作と同じ)
.buildConstraintViolationWithTemplate(hibernateContext.getDefaultConstraintMessageTemplate())
// 制約違反情報を追加する
.addConstraintViolation();
}
return result;
}

上記のような実装にすることで、メッセージにプロパティキーではなく、プロパティキーに対応する値を埋め込むことができるようになります。この仕組みを活用すると、メッセージの中に「チェックがOKとなる日付」を埋め込むこともできるようになります。

public boolean isValid(LocalDate targetValue, ConstraintValidatorContext context) {

LocalDate validMinDate = LocalDate.now().plusDays(value);
boolean result = // チェック内容は省略...

if (!result) {
HibernateConstraintValidatorContext hibernateContext =
context.unwrap(HibernateConstraintValidatorContext.class);
hibernateContext.disableDefaultConstraintViolation();
hibernateContext
.addMessageParameter("days", this.days)
.addExpressionVariable("days", this.days)
// チェックOKになる日付を追加
.addMessageParameter("validMinDate", validMinDate)
.addExpressionVariable("validMinDate", validMinDate)
.buildConstraintViolationWithTemplate(hibernateContext.getDefaultConstraintMessageTemplate())
.addConstraintViolation();
}
return result;
}


src/main/resources/ValidationMessages.properties

com.example.validation.DaysLater.message = must be {validMinDate} or later


制約違反した際のエラーメッセージは「must be 2018-09-26 or later」となり、より直感的なメッセージにすることができます。


まとめ

Hibernate Validator上で動かすことが前提条件となっているのであれば、今回紹介した拡張機能を使うことでユーザビリティの高いメッセージを組み立てやすくなると思われます。今携わっている案件はHivernate Validatorでの実行を前提として良いので、この仕組みを有効に活用しようと思っています。