結論
ecuacion-lib-validation と言う Jakarta Validation ベースのライブラリには、List などの Collection や 配列の要素に対する validation で、項目名つきメッセージを出力する機能があります、と言うお話。
前提知識
ExceptionUtil の理解を前提の記事とさせていただいております m(_ _ )m
ecuacion-lib-validation の導入方法含め、下記記事を先にご覧くださいませ。
-
ExceptionUtilとそれを使用したメッセージへの項目名追加
(導入方法は「ecuacion-lib-validationの導入方法」の項をご参照ください)
- 項目名の有無のデフォルト設定と個別指定
はじめに
Bean Validation 改め Jakarta Validation では、List に対する validation も可能です。
例えば、こう言う validation ですね。
それぞれの社員が、複数のノートPCを保持する、と。model は機種名のことです。
public record Employee(String name, List<@Valid Laptop> laptopList) {
}
public record Laptop(int id, @NotNull String model) {
}
Employee の List<Laptop> に @Valid が付加されているので、List の要素である各 laptop に対しても validation が実施されます。
そして、validation を実行するコードはこちら。
public static void 基本的な使い方() {
List<Laptop> laptopList = new ArrayList<>();
laptopList.add(new Laptop(1, null));
Set<ConstraintViolation<Employee>> set = validator.validate(new Employee("John", laptopList));
for (String message : ExceptionUtil.getMessageList(set)) {
System.out.println(message);
}
}
これを普通に実行した結果は、当たり前ですが以下。
入力必須です
これではなんだかわからないので項目名を出したいのですが、さて、List の要素から発生するエラーのメッセージにおける項目名は、一体どのような文言にしたら良いのでしょう・・・?
List の要素のエラーでのわかりやすいメッセージ
まず第一歩は、こちら・・・?
機種名は入力必須です
普通に項目名を入れるとこうなる。でもこれではわからない。
ノートPCは複数持てるのでどのPCかの明示が必要なのと、他に携帯も持っていて携帯の機種名もあると、さらにわからない。
それらを考慮した、人が見てわかるメッセージとは、以下の感じかなと。
1番目のノートPCの機種名は入力必須です
ちなみに数字だけ見るとわかりませんが、コンピュータ上「0番目」で表されるものを、「1番目の〜」というメッセージで表現しているつもりです。つまり1番始まり。
人間には絶対そっちの方がわかりやすいよね・・・
可能ならこうしたいけど・・・
ちなみに、必ず入力されていることがわかる項目があれば、それを表示するのが一番わかりやすいです。
たとえばこんな。
ID=1のノートPCの機種名は入力必須です
でも、新規登録の場合などはID項目含めて空欄の場合もあるし、どの項目をメッセージに表示するか、の情報も必要になり複雑なので、実用性も鑑みると「1つめの〜」くらいの表現が妥当かと思われます。
validationの分割による解決
メッセージのイメージがついたところで、具体的な実装について。
先に言っておくと、綺麗にカスタマイズしたメッセージを出したい場合は、validation を分割実施する方法があります。
employee と laptop の validation を別で実施し、laptop の validation を実施する際に、わかりやすいメッセージを付加する、と言う方法です。
private static void validationの分割による解決() {
try {
EmployeeWithoutValid employee =
new EmployeeWithoutValid("John", List.of(new Laptop(1, null)));
// employee の validation
ValidationUtil.validateThenThrow(employee);
// laptop の validation
for (int i = 0; i < employee.laptopList().size(); i++) {
MessageParameters params =
ValidationUtil.messageParameters().messagePrefix((i + 1) + "番目のノートPCについて、");
ValidationUtil.validateThenThrow(employee.laptopList().get(i), params);
}
} catch (ConstraintViolationException ex) {
for (String message : ExceptionUtil.getMessageList(ex, true)) {
System.out.println(message);
}
}
}
実行結果はこちら。
1番目のノートPCについて、「機種」は入力必須です。
ただし、上記例だと employee と laptop の両方でエラーがあった場合に両方のエラーを同時にthrow することができません。
それも、それぞれで発生した ConstraintViolation を merge してから throw すれば対応は可能ではあります。
ただ、毎回これを書かなきゃいけないのは面倒ですね・・・
ValidationUtil・ExceptionUtil でのメッセージ出力
ValidationUtil と ExceptionUtil では、こんな List の validation に対応したメッセージを出力する機能があります。
基本的な使い方
validation を実行するためのコードはこちら。
public static void 基本的な使い方() {
Employee employee = new Employee("John", List.of(new Laptop(1, null)));
try {
MessageParameters params = ValidationUtil.messageParameters().showsItemNamePath(true);
ValidationUtil.validateThenThrow(employee, params);
} catch (ConstraintViolationException ex) {
for (String message : ExceptionUtil.getMessageList(ex, true)) {
System.out.println(message);
}
}
}
showsItemNamePath という parameter を指定しています。itemName の経路の表示有無設定ですね。true にすると表示されます。
この指定は、{0} があったときに初めて機能するものなので、併せて ExceptionUtil.getMessageList の引数に、項目名を表示するための true を指定しています。
結果、以下のようなメッセージが得られます。
「employee.laptopList」の1番目の要素の「laptop.model」は入力必須です。
「」で括られた itemNameKey は、 messages.properties または item_names.properties にこのキーで定義をすることでラベル名が表示されます。
laptop.model=機種
employee.laptopList=社員保持ノートPC(複数)
すると、結果はこうなります。
「社員保持ノートPC(複数)」の1番目の要素の「機種」は入力必須です。
showsItemNamePath に false を指定した(=デフォルト)場合
showsItemNamePath に唐突感があったので、もう少し補足しておきます。
showsItemNamePath に false を設定した場合(あるいはそもそも showsItemNamePath の設定をしない場合)の実行結果は以下になります。
「機種」は入力必須です。
ここでの「機種」は、「社員が保持する」という文脈を無視した、laptop.model に対する項目名です。
「社員が保持するノートPC」、「倉庫の棚に保管されているノートPC」など、文脈は状況によりますが、いちいちこのような文脈を加味した項目名(?)をつけるのは煩雑です。
なので ecuacion-lib-validation では、文脈を無視した項目名と、その文脈を分けて管理しています。
itemNamePath は、その文脈部分を表しています。
「項目名のパス」という意味ですが、「社員が保持する」というのは、laptop の上位に存在する Employee の情報のことです。
すなわち、 propertyPath をたどりながら、そのそれぞれの node に対する項目名を並べたものが「文脈」となることから、itemName の path という表現になっています。
複数階層のList
Employee の上位階層として、部署を表す Dept を追加します。
public record Dept(String name, List<@Valid Employee> employeeList) {
}
こんなコードで実行します。
private static void 複数階層のList() {
List<Laptop> laptopList = List.of(new Laptop(1, null));
Dept dept = new Dept("Sales",
List.of(new Employee("John", List.of()), new Employee("Paul", laptopList)));
try {
MessageParameters params = ValidationUtil.messageParameters().showsItemNamePath(true);
ValidationUtil.validateThenThrow(dept, params);
} catch (ConstraintViolationException ex) {
for (String message : ExceptionUtil.getMessageList(ex, true)) {
System.out.println(message);
}
}
}
こちらも記載。
dept.employeeList=部署所属社員
実行結果はこちら。
「部署所属社員」の2番目の要素の中の、「社員保持ノートPC(複数)」の1番目の要素の「機種」は入力必須です。
List<String> などの取り扱い
アカウントに複数メールアドレスがある、と言うパターン。
List の要素に対して @NotNull をつけています。
public record Account(String name, @NotNull List<@NotNull String> mailAddresses) {
}
実行用コードはこちら。
private static void List_String_の取り扱い() {
List<String> mailAddressList = Arrays.asList("test@test.com", null);
Account account = new Account("John", mailAddressList);
try {
MessageParameters params = ValidationUtil.messageParameters().showsItemNamePath(true);
ValidationUtil.validateThenThrow(account, params);
} catch (ConstraintViolationException ex) {
for (String message : ExceptionUtil.getMessageList(ex, true)) {
System.out.println(message);
}
}
}
こちらも記載。
account.mailAddresses=メールアドレス一覧
実行結果はこちら。
「メールアドレス一覧」の2番目の要素は入力必須です。
「メールアドレス一覧」は入力必須です。
サンプルコード
サンプルコードは以下です。
https://github.com/ecuacion-jp/ecuacion-code-snippets/tree/main/ecuacion-lib-core-JakartaValidationList
※このページから直接ソースの zip を download はできないと思うので、そのページにある ecuacion-code-snippets のリンクをクリックし、そこにある緑の <> Code ボタンから Download ZIP で download してください。
本サンプルのフォルダに移動後、mvn compile exec:java で実行できます。
まとめ
Jakarta Validation を使用しメッセージに項目名(フィールド名)含める、ecuacion-lib-validation のご紹介でした。