SpringBootでGETのリクエストパラメータのバリデーションをしてバリデーションメッセージを返却するまで

## 概要
Spring Boot でGETのリクエストパラメータのバリデーションのやり方と、バリデーションに引っかかった際に投げられる例外のハンドリングのやり方、そしてレスポンスにバリデーションのエラーメッセージを含めるやり方のメモ。

## 前提
- `@RestController`を用いたJSON API
- GETでパラメータを渡す。そのパラメータにバリデーションをかける

## 環境
Java: 1.8
SpringBoot: 1.5.7

## やってみる
Spring Initializerで適当にProjectを作っておく。
DependencyにはWebとlombockを指定する。


### まずバリデーションなしバージョン

以下のようなコントローラを作る。
`/hello`にパラメータにメールアドレスを渡してアクセスしたら挨拶が返ってくるだけ。

```java
package com.example.sample;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/hello")
public class HelloController {

    @GetMapping
    public String get(@RequestParam("email") String email) {
        return "Hello!, " + email;
    }
}
```

起動して叩いてみる
メールアドレスではない文字列を渡しても、当然素通りしてしまう。

```bash
$ curl "http://localhost:8080/hello?email=hogehoge"
Hello, hogehoge
```

ここにバリデーションを足してみる。

### バリデーションありバージョン

#### 実装方法
コントローラを修正する

```java
package com.example.sample;

import org.hibernate.validator.constraints.Email;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;

@RestController
@RequestMapping("/hello")
@Validated  // 追加
public class HelloController {

    @GetMapping
	  // @Emailと@Validを追加
    public String get(@Email @Valid @RequestParam("email") String email) {
        return "Hello!, " + email;
    }
}
```


叩いてみる。

妥当なメールアドレスを渡せば正常なレスポンスが帰ってくる。
無効なメールアドレスを渡すとExceptionの内容が返ってくる。

```bash
$ curl "http://localhost:8080/hello?email=hogehoge"      
{"timestamp":1507806432128,"status":500,"error":"Internal Server Error","exception":"javax.validation.ConstraintViolationException","message":"No message available","path":"/hello"}
```

これでバリデーションはできた。

#### 解説
- `@Validated`アノテーションをコントローラの定義文のところに追加するとバリデーションが効くようになる。
(POSTの＠RequestBodyに対してバリデーションする際はいらなかったので少し混乱した。[この記事](https://sdqali.in/blog/2015/12/04/validating-requestparams-and-pathvariables-in-spring-mvc/)を参考にGETパラメータに対してバリデーションする際は`@Validated`アノテーションが必要だとわかった。
- バリデーション対象のパラメータ(今回は`@RequestParam("email") String email`)の前に`@Valid`アノテーションをつけて、重ねて任意のバリデーション用のアノテーションを付与するとバリデーションが動くようになる。今回は`@Email`アノテーションを付与した。

### 例外をハンドリングする
現状ではバリデーションに引っかっかった際のレスポンスの中身がカスタマイズできない。
バリデーションに引っかかった際に投げられたExceptionの中身をSpringがよしなにJSONに変換して返してくれているだけ。
自分で例外をキャッチして任意のフォーマットでレスポンスを返せるようにする。

#### 実装方法

```java
package com.example.sample;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.validation.ConstraintViolationException;

@RestControllerAdvice
public class HelloExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler
    public ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
        return super.handleExceptionInternal(ex, "validation error", null, HttpStatus.BAD_REQUEST, request);
    }
}

```

叩いてみる。
バリデーションに引っかかると validation errorと返ってくる

```bash
$ curl "http://localhost:8080/hello?email=hogehoge"      
validation error
```

これで任意のフォーマットでレスポンスを返せるようになった。

#### 解説
例外ハンドラ用のクラスを作っている。
`ResponseEntityExceptionHandler`クラスを継承して、`@RestControllerAdvice`アノテーションを付与する。
中にバリデーションに引っかっかった際にスローされる例外をハンドリングするメソッドを作る。
`@ExceptionHandler`アノテーションを付与して、第一引数にキャッチしたい例外の型を指定すると、その例外が投げられたときにそのメソッドが呼ばれる。

### レスポンスの中身を親切にする
バリデーションに引っかっかった際にバリデーションエラーの内容をレスポンスに含めてみる。

#### 実装方法

レスポンス用のクラスを作る

```java
package com.example.sample;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Getter
@RequiredArgsConstructor
public class ApiValidationErrorResponse implements Serializable {

    private final String message;

    @Getter
    @RequiredArgsConstructor
    private static class ValidationMessage implements Serializable {
        private final String message;
    }

    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<ValidationMessage> validationMessages = new ArrayList<>();

    public void addValidationMessage(String message) {
        validationMessages.add(new ValidationMessage(message));
    }
}
```


`HelloExceptionHandler`クラスを修正する。

```java
package com.example.sample;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.validation.ConstraintViolationException;

@RestControllerAdvice
public class HelloExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler
    public ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, WebRequest request) {
        ApiValidationErrorResponse error = new ApiValidationErrorResponse("validation error");
        ex.getConstraintViolations().forEach(v -> error.addValidationMessage(v.getMessage()));
        return super.handleExceptionInternal(ex, error, null, HttpStatus.BAD_REQUEST, request);
    }
}
```

叩いてみる。
レスポンスにどんなバリデーションに引っかかったのか、詳細な情報が含まれている。

```bash
$ curl "http://localhost:8080/hello?email=h"
{"message":"validation error","validationMessages":[{"message":"not a well-formed email address"}]}
```

#### 解説
- レスポンス用のクラス`ApiValidationErrorResponse`を作っている。`Serializable`インタフェースを継承しておけば、よしなにJSONに変換してくれる。
- lombock使ってます。使わない場合は自分でゲッターやコンストラクタ書いてください。
- `@JsonInclude(JsonInclude.Include.NON_EMPTY)`の部分は、バリデーションメッセージが空のときはvalidationMessageフィールドをJSONに含めないようにするためのアノテーションです。
- `handleConstraintViolationException`メソッドの中で上で作ったレスポンス用のクラスを生成している。


## 参考
- [Validating RequestParams and PathVariables in Spring MVC - {code that works} by Sadique Ali](https://sdqali.in/blog/2015/12/04/validating-requestparams-and-pathvariables-in-spring-mvc/)
- Spring徹底入門 株式会社NTTデータ 2016