---
title: Spring 6.1でYAVIがSpring MVCの@Valid/Validatedで使いやすくなった
tags: ["Java", "YAVI", "Spring Boot", "Spring MVC"]
categories: ["Programming", "Java", "org", "springframework", "validation"]
date: 2023-07-10T07:16:44Z
updated: 2023-07-10T07:35:31Z
---

Spring 6.1.0-M1でFunctionalなValidator factory methodが追加されました (https://github.com/spring-projects/spring-framework/pull/29890) 。


これにより、YAVIのValidatorからSpringのValidatorへの変換が容易になりました。そのため、`@Valid`/`@Validated`でValidationを行なっているControllerでYAVIが使いやすくなりました。


[Springのガイド](https://spring.io/guides)のうち"Validating Form Input" (https://spring.io/guides/gs/validating-form-input/) を例にBean ValidationをYAVIに変更してみます。

まずはValidationの定義。

元の定義は

```java
package com.example.validatingforminput;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class PersonForm {

	@NotNull
	@Size(min=2, max=30)
	private String name;

	@NotNull
	@Min(18)
	private Integer age;

	// ...
}
```

です。

YAVIで書くと、例えば


```java
package com.example.validatingforminput;

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.Validator;

public class PersonForm {

	public static Validator<PersonForm> validator = ValidatorBuilder.<PersonForm>of()
			.constraint(PersonForm::getName, "name", c -> c.notNull().greaterThanOrEqual(2).lessThanOrEqual(30))
			.constraint(PersonForm::getAge, "age", c -> c.notNull().greaterThanOrEqual(18))
			.build();

	// ...
}
```


になります。



ControllerでのValidationの利用は、元のコードは

```java
package com.example.validatingforminput;

import jakarta.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Controller
public class WebController implements WebMvcConfigurer {

	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/results").setViewName("results");
	}

	@GetMapping("/")
	public String showForm(PersonForm personForm) {
		return "form";
	}

	@PostMapping("/")
	public String checkPersonInfo(@Valid PersonForm personForm, BindingResult bindingResult) {

		if (bindingResult.hasErrors()) {
			return "form";
		}

		return "redirect:/results";
	}
}
```

です。

YAVI + Spring 6.1を使った場合、次のように書けます。


```java
package com.example.validatingforminput;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Controller
public class WebController implements WebMvcConfigurer {

	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/results").setViewName("results");
	}

	@InitBinder
	void initBinder(WebDataBinder binder) {
		Validator personValidator = Validator.forInstanceOf(PersonForm.class, PersonForm.validator.toBiConsumer(Errors::rejectValue));
		binder.addValidators(personValidator);
	}

	@GetMapping("/")
	public String showForm(PersonForm personForm) {
		return "form";
	}

	@PostMapping("/")
	public String checkPersonInfo(@Validated PersonForm personForm, BindingResult bindingResult) {

		if (bindingResult.hasErrors()) {
			return "form";
		}

		return "redirect:/results";
	}
}
```


以下の部分がSpring 6.1で利用可能になったfactory methodを利用しています。

```java
Validator personValidator = Validator.forInstanceOf(PersonForm.class, PersonForm.validator.toBiConsumer(Errors::rejectValue));
```

上記の例ではBean Validationおよび`spring-boot-starter-validation`を依存関係から除外しています。そのため`@Valid` (Bean Validationのアノテーション)の代わりに`@Validated` (Springのアノテーション)を使用しています。どちらも利用可能です。


全体のDiffは https://github.com/making/gs-validating-form-input/commit/7261b26bf94c4aae86b52c68f9f78380e07a79f3 です。

ControllerだけのDiffは次の図の通りです。

<img width="1024" alt="image" src="https://github.com/making/blog.ik.am/assets/106908/0c72d61a-156c-4a9d-a49c-63b42475c467">

Controllerのコードは大きく変えることなくBean Validationの代わりにYAVIを使用することが可能になりました。

---

ちなみに[ドキュメント](https://yavi.ik.am/#integrations)に記載されている通り、これまでもYAVIはSpring MVCで容易に利用可能です。


今回の例で言うと以下のように書けます。

```java
	@PostMapping("/")
	public String checkPersonInfo(@Validated PersonForm personForm, BindingResult bindingResult) {
		ConstraintViolations violations = PersonForm.validator.validate(personForm);
		if (!violations.isValid()) {
			violations.apply(bindingResult::rejectValue);
			return "form";
		}

		return "redirect:/results";
	}
```

or

```java
	@PostMapping("/")
	public String checkPersonInfo(@Validated PersonForm personForm, BindingResult bindingResult) {
		return PersonForm.validator.applicative()
				.validate(personForm)
				.fold(violations -> {
					ConstraintViolations.of(violations).apply(bindingResult::rejectValue);
					return "form";
				}, form -> "redirect:/results");
	}
```

今回の更新は`@Valid`/`@Validated`によるプログラミングモデルが好きな場合に特に親和性が高くなったという話でした。
