SpringBootで作成したREST APIのリクエストに対するバリデーションアノテーションの付け方

SpringBootで作成したREST APIのリクエストに対してバリデーションチェックを行うにあたって、`@Validated`と`@Valid`の付与の仕方を整理するためにメモ。
# 動作環境
- Spring Boot : 2.6.1
- Dependenciesにspring-boot-starter-web,spring-boot-starter-validationが必要です（[spring initializer](https://start.spring.io/#!type=maven-project&language=java&platformVersion=2.6.1&packaging=jar&jvmVersion=11&groupId=com.example&artifactId=demo&name=demo&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.demo&dependencies=web,validation)）。

# `@PathVariable`と`@RequestParam`に対してのバリデーションチェック
- クラスに対して`@Validated`を付与。
- 引数にバリデーション用のアノテーション（`@Max`等）を付与。

## サンプルコード
```java:SampleRestController.java
@Validated //ここに付与
@RestController
@RequestMapping("/validTest")
public class SampleRestController {

	@GetMapping("/pathValid/{limit}")
	public ResponseEntity<String> pathValid(@PathVariable("limit") @Min(1) @Max(5) Integer limit) {
		return ResponseEntity.ok("path validation OK!");
	}

	@GetMapping("/paramValid")
	public ResponseEntity<String> paramValid(@RequestParam("message") @Size(min = 1, max = 5) String message) {
		return ResponseEntity.ok("param validation OK!");
	}

}
```

## 動作確認
### `@PathVariable`
#### 正常値 

```sh
> curl localhost:8080/validTest/pathValid/3
path validation OK!
```

#### 異常値

```sh
> curl localhost:8080/validTest/pathValid/6
{"timestamp":"2021-12-12T12:47:06.657+00:00","status":500,"error":"Internal Server Error","path":"/validTest/pathValid/6"}
```
ログ

```console
2021-12-12 21:47:06.647 ERROR 8812 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: pathValid.limit: 5 以下の値にしてください] with root cause

javax.validation.ConstraintViolationException: pathValid.limit: 5 以下の値にしてください
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.13.jar:5.3.13]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.13.jar:5.3.13]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.13.jar:5.3.13]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.13.jar:5.3.13]
	at com.example.demo.SampleRestController$$EnhancerBySpringCGLIB$$424b5c2e.pathValid(<generated>) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
    ...

```
上記よりバリデーションチェックが有効になっていることがわかる。

---
### `@RequestParam`
#### 正常値 

```sh
> curl localhost:8080/validTest/paramValid?message=hello
param validation OK!
```

#### 異常値

```sh
> curl localhost:8080/validTest/paramValid?message=message
{"timestamp":"2021-12-12T12:48:51.798+00:00","status":500,"error":"Internal Server Error","path":"/validTest/paramValid"}
```

ログ

```console
2021-12-12 21:48:51.797 ERROR 8812 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: paramValid.message: 1 から 5 の間のサイズにしてください] with root cause

javax.validation.ConstraintViolationException: paramValid.message: 1 から 5 の間のサイズにしてください
	at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.13.jar:5.3.13]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.13.jar:5.3.13]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.13.jar:5.3.13]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.13.jar:5.3.13]
	at com.example.demo.SampleRestController$$EnhancerBySpringCGLIB$$424b5c2e.paramValid(<generated>) ~[classes/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
    ...
```

# `@RequestBody`に対してのバリデーションチェック
- 引数に対して`@Valid`を付与。

## サンプルコード
### コントローラ
```java:SampleRestController.java
@RestController
@RequestMapping("/validTest")
public class SampleRestController {

	@PostMapping("/reqBodyValid")
	public ResponseEntity<String> reqBodyValid(@RequestBody @Valid User user) {
		return ResponseEntity.ok("RequestBody validation OK!");
	}
}
```

### バリデーション対象クラス
```java:User.java
public class User {

	@Min(1)
	@Max(15)
	int id;

	@NotNull
	@Size(min = 1, max = 20)
	String name;

	@Min(1)
	int age;

    //以下getter,setter
}
```

## 動作確認
#### 正常値 

```sh
> curl -XPOST localhost:8080/validTest/reqBodyValid -H "Content-Type: application/json" -d '{\"id\":1,\"name\":\"user\",\"age\":20}'
RequestBody validation OK!
```

#### 異常値

```sh
> curl -XPOST localhost:8080/validTest/reqBodyValid -H "Content-Type: application/json" -d '{\"id\":0,\"name\":\"userrrrrrrrrrrrrrrrr\",\"age\":0}'
{"timestamp":"2021-12-12T14:38:50.137+00:00","status":400,"error":"Bad Request","path":"/validTest/reqBodyValid"}
```
ログ

```console
2021-12-12 23:38:50.136  WARN 20332 --- [nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<java.lang.String> com.example.demo.SampleRestController.reqBodyValid(com.example.demo.User) with 2 errors: [Field error in object 'user' on field 'age': rejected value [0]; codes [Min.user.age,Min.age,Min.int,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age],1]; default message [1 以上の値にしてください]] [Field error in object 'user' on field 'id': rejected value [0]; codes [Min.user.id,Min.id,Min.int,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.id,id]; arguments []; default message [id],1]; default message [1 以上の値にしてください]] ]
```
上記よりバリデーションチェックが有効になっていることがわかる。

# おわりに
バリデーションチェックに関して検索すると、全部のせみたいな感じのサンプルコードが多く、整理してみると付け方を間違えているものがあった。

# 参考資料
- https://reflectoring.io/bean-validation-with-spring-boot/