Java: Spring APIのバリデーション実装について

## はじめに
- とあるプロジェクトでJava × SpringでAPIを製造したのですが、その際にバリデーションについて調査したので、備忘録として内容をまとめてみました
- Java × Spring でAPIを作成した際にリクエスト段階でバリデーションチェックをしたいときに有効

## 環境
- Java 11
- Spring Boot 2系
- Lombok

## 前提
- Java × Spring でWeb APIを実装
- APIリクエスト時に、RequestDTOを用いて**送信されるJSONリクエストをRequestDTOにマッピングする想定**

## Spring / APIのバリデーション実装
- Springは、Java標準であるJSR-303のBean Validationをサポートしています。Spring MVCでの開発においては、Bean Validationを利用してバリデーションをする方法が推奨されています。

※ **JSR-303:Bean Validation**は「**JavaBeansのプロパティが取り得る値や条件を、制約のアノテーションとして設定することで、バリデーション内容を定義する仕組み**」です。バリデーション仕様を実装したライブラリを使って、バリデーションを実装していきます。


#### 参考
[Bean Validation 公式ページ](https://beanvalidation.org/)

[Bean Validationの基本](http://enterprisegeeks.hatenablog.com/entry/2016/02/15/072944)


Spring Frameworkで使用できるBean Validationアノテーション一覧は下記のサイトを参照  
[Spring Frameworkで使用できるBean Validationアノテーション一覧](https://penguinlabo.hatenablog.com/entry/springframework/validator)


## 実装方法
- バリデーションチェック実装の流れを順を追って確認

## 1. 入力チェックの有効化/DTO作成
- Spring MVCのデフォルト動作では、**リクエストパラメータに対する入力チェックは実行されません。**
- 入力チェックを行う場合には、入力チェックを行うメソッドの引数にリクエストパラメータをマッピングするDTOクラスを定義し、**@org.springframework.validation.annotation.Validated（@Validated）**　を指定します。（指定する場所はControllerのメソッド内）

#### サンプルDTO: UserRequest.java
- DTOクラス。各フィールドに適用したいルール（@アノテーション）を指定する。
- 具体的なアノテーションについては [Spring Frameworkで使用できるBean Validationアノテーション一覧](https://penguinlabo.hatenablog.com/entry/springframework/validator)を参照

```java
package jp.XXXX.controller;

import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * サンプル：ユーザー情報取得時DTOクラス
 * 
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRequest {

  @NotNull(message = "ユーザーIDは必須です。")
  @DecimalMax(value = "999999999", message = "ユーザーIDは{value}以下を指定してください。")
  private Integer userId;
}

```
#### サンプル: UserController.java #getUserInfoById()
- SpringのControllerでは、 **@RequestBody**を使用することでHTTPリクエストボディに設定されたパラメータをJSONで受け取り、受取ったJSONを指定したDTOクラスにマッピングします。
- サンプルでは、POSTメソッドを想定
- **@org.springframework.validation.annotation.Validated（@Validated）** はバリデーションの有効化を定義
- **@org.springframework.validation.BindingResult（BindingResult）** には、リクエストデータのバインディングエラーと入力チェックエラー情報が格納されます。

```java
package jp.XXXX.controller;

中略

  /**
   * ユーザーID指定で、ユーザー情報を取得
   *
   * @return ユーザー情報取得
   */
  @PostMapping("/userInfo")
  public ResponseEntity<User> getUserInfoById(
      @RequestBody @Validated UserRequest request, BindingResult result)
      throws BindException {
```

## 2. 入力チェック結果の判定
- 入力チェックの実行結果を取得し、エラーがあった場合はエラー結果（`BindingResult`）を**BindException**にスローします。

```java
  @PostMapping("/userInfo")
  public ResponseEntity<User> getUserInfoById(
      @RequestBody @Validated UserRequest request, BindingResult result)
      throws BindException {
    if (result.hasErrors()) {
      throw new BindException(result);
    }
```

- BindException の詳細は [BindException](https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/BindException.html) を参照

## 単体テスト
- 各フィールドに適用したバリデーションルールが効いているか、単体テストコードで確認する。以下、DTOクラスを対象したテストコード例。


#### サンプル: UserRequestTest.java

```java
package jp.XXXX.controller;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;


@SpringBootTest
public class UserRequestTest {

  // Validatorの実行クラス
  private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

  @Test
  public void testOk() {

    UserRequest requestDto =
        new UserRequest(Integer.valueOf(1));

    Set<ConstraintViolation<UserRequest>> violations =
        validator.validate(requestDto);
    assertThat(violations.size()).isEqualTo(0);
  }

  @Test
  public void testNotNull_1() {

    UserRequest requestDto = new UserRequest();

    Set<ConstraintViolation<UserRequest>> violations =
        validator.validate(requestDto);
    assertThat(violations.size()).isEqualTo(1);

    assertThat(violations).extracting("message").containsOnly("ユーザーIDは必須です。");
  }

  @Test
  public void testNotNull_2() {

    UserRequest requestDto =
        new UserRequest(null);

    Set<ConstraintViolation<UserRequest>> violations =
        validator.validate(requestDto);
    assertThat(violations.size()).isEqualTo(1);

    assertThat(violations).extracting("message").containsOnly("ユーザーIDは必須です。");
  }

  @Test
  public void testDecimalMax() {

    UserRequest requestDto =
        new UserRequest(1234567890);

    Set<ConstraintViolation<UserRequest>> violations =
        validator.validate(requestDto);
    assertThat(violations.size()).isEqualTo(1);

    assertThat(violations).extracting("message").containsOnly("ユーザーIDは999999999以下を指定してください。");
  }
}

```

## 最後に
- Java × Spring でサクッとAPI作れます！！