【Spring Boot】バリデーションで詰まったことまとめ

## はじめに
Spring Bootで個人開発をしており、バリデーションで詰まったところをまとめます（Spring Bootの実務経験はありません）。
基本、以下を参考にしています。
javax→jakartaに変わったのですが、それ以外は変わっていないようです。
[【Spring Boot】バリデーション](https://b1san-blog.com/post/spring/spring-validation/)

## Spring Boot3系にバージョンアップしたらjavax.validationがエラーになる
元々javax.validationでバリデーションを行っていたのですが、
ある日アプリケーションの起動ができなくなり、プロジェクトを新規作成した際にSpring Boot2.7.3→3.2.0にバージョンアップしました。
すると、importのjavax.validation〜がエラーになっていました。
調べるとjavax→jakartaになったことが出てきますが、書き換えてもエラーのままでした。
pom.xmlに以下が必要でした。
元のプロジェクトには書いてあったので、新規作成した際に漏れていました。
```pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
```

## 数値項目のバリデーション
Formクラスにバリデーションを入れています。
数値項目をintとすると、入力値がnullだった際に以下のエラーメッセージが表示されました。
![スクリーンショット 2023-12-31 12.43.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3097017/da459101-f2b7-446e-fe5f-45cfdc30051b.png)
> Failed to convert property value of type java.lang.String to required type int for property [項目名]; For input string: ""

```ReportForm.java
@Data
public class ReportForm {
    // 作業内容（String項目はこのように必須チェック）
    @NotBlank(groups = ValidGroup1.class, message = "入力してください")
    private String workContent;
    
    // 難易度
    @PositiveOrZero(groups = ValidGroup1.class, message = "入力してください")
    @Pattern(regexp="^[0-9]{1,3}$", groups = ValidGroup2.class, message = "半角数字3桁以内で入力してください")
    private int difficulty;
}
```

messages.propertiesでメッセージをカスタムできるかと思いきやできませんでした。
アノテーションもいろいろ試してみました。
String型にして登録する時にintに変換すれば期待のメッセージ表示となりました。
![スクリーンショット 2023-12-31 12.46.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3097017/8027dbe6-4333-ecf2-456c-68781b5115f7.png)

```ReportForm.java
// 難易度
@NotBlank(groups = ValidGroup1.class, message = "入力してください")
@Pattern(regexp="^[0-9]{1,3}$", groups = ValidGroup2.class, message = "半角数字3桁以内で入力してください")
private String difficulty;
```
また、Integerにすれば期待のメッセージ表示となりました。
```ReportForm.java
// 難易度
@NotNull(groups = ValidGroup1.class, message = "入力してください")
@Range(min = 0, max = 999, groups = ValidGroup2.class, message = "半角数字3桁以内で入力してください")
private Integer difficulty;
```
ちなみに```@Range```ではなく```@Pattern```を使うと以下のエラーとなります。
文字列でないと使えないんですね。
> No validator could be found for constraint 'jakarta.validation.constraints.Pattern' validating type 'java.lang.Integer'. Check configuration for [項目名]

※追記：しかし、```@Range```のチェックだと数値ではなく「a」などを入力すると以下のメッセージになります。
FormはString型にして```@Pattern```による正規表現のチェックをした後、modelに入れる際、数値に変換するのがよいかもしれません。
> Failed to convert property value of type java.lang.String to required type java.lang.Integer for property difficulty; For input string: "a"

例えば、以下は、入力された時だけ半角数字3桁かチェックします。
（入力なしの場合は空文字で渡されるので空文字を許容する正規表現としています）
```java
// 表示順
@Pattern(regexp = "^$|^[0-9]{1,3}$", message = "半角数字3桁で入力してください")
private String displayOrder;
```
この後、登録の際は数値に変換してmodelに設定
```java
model.setDisplayOrder(Integer.parseInt(form.getDisplayOrder()));
```

型によって使えるアノテーションについては以下が分かりやすいかなと思いました。
[Spring Frameworkで使用できるBean Validationアノテーション一覧](https://penguinlabo.hatenablog.com/entry/springframework/validator)

doubleも同様なためDoubleにする必要があります。
```ReportForm.java
// 平均残業時間
@NotNull(groups = ValidGroup1.class, message = "入力してください")
@Digits(integer = 2 , fraction = 2, groups = ValidGroup2.class, message = "整数部2桁、少数部2桁以内で入力してください")
private Double aveOvertimeHours;
```

記事によってはしれっとIntegerを使っているのでその通りにやれば問題なくできると思います。
なんでInteger？と思い調べたところ、intとIntegerの違いを知りました。
- int：null不可（プリミティブ型）
- Integer：null許可（参照型）

Integerはintのラッパー型というだけの知識で、Listを使う時とStringを数値に変換する時に使うだけでした。
最近C#が長かったのでnull許容か否かというのは分かりやすいです。

## 優先順位をつける
まず必須チェックを行い、次に文字種チェックを行いたいというような場合です。
```@GroupSequence```というのを使い、ValidGroup1を必須チェック、ValidGroup2を桁数チェック、ValidGroup3を文字種チェックとします。
```java
import jakarta.validation.GroupSequence;

@GroupSequence({ValidGroup1.class, ValidGroup2.class, ValidGroup3.class})
public interface GroupOrder {
}

public interface ValidGroup1 {
}
public interface ValidGroup2 {
}
public interface ValidGroup3 {
}
```
```UserForm.java
@Data
public class UsersForm {
    // パスワード
    @NotBlank(groups = ValidGroup1.class, message = "入力してください")
    @Pattern(regexp="(^$|.{8,10})", groups = ValidGroup2.class, message = "半角英数字8文字以上10文字以内で入力してください")
    @Pattern(regexp="(^$|^[A-Za-z0-9]+$)", groups = ValidGroup3.class, message = "半角英数字で入力してください")
    private String password;
}
```

ただ、ある項目は入力されているのに桁数が間違っている場合、桁数エラーが出ないことがあります。
ValidGroup1が設定されている項目が設定されている項目が全てOKじゃないと、ValidGroup2のエラーが出ないようです。
ネットで調べても「そういう仕様」というところまでしか分かりません。

細かい要件があるとうまく実現できません。
例えば以下のように新規登録画面と更新画面を同じ画面で行う場合です。
・新規登録の場合、パスワードの必須チェックを行う
・更新の場合、入力されている時だけパスワードの文字種チェックを行う
→パスワードの必須チェックをValidGroup1とは別の登録グループに入れたのですが、他項目の必須チェックと同時にメッセージが出せません。
良い方法はないだろうかと模索中です。
実務ではどのようにしているのでしょうか。