---
title: YAVIでValue ObjectのValidation (Records対応)
tags: ["Java", "YAVI"]
categories: ["Programming", "Java", "am", "ik", "yavi"]
date: 2021-05-22T15:21:31Z
updated: 2021-05-22T15:47:42Z
---

[YAVI](https://github.com/making/yavi) 0.6.0で [Applicative Functor](https://en.wikipedia.org/wiki/Applicative_functor) を使用したValidationに対応しました。
特にValue ObjectのValidationを組み合わせる場合に有用です。(もちろんValue Object以外でも有用です。)

> YAVIとは何?という場合は"[Java用Validatorライブラリ"YAVI"(ヤヴァイ)の紹介](/entries/461)"を参照してください。

導入の背景を含めて何が嬉しいかは後述し、まずは使い方を紹介します。

### 使い方

`Email`と`PhoneNumber`というValue Objectクラスがあり、これらをフィールドに持つ`ContactInfo`クラスがあるとします。
折角なのでJava 16でサポートされた [Records](https://openjdk.java.net/jeps/395) を使用して定義します。

```java
public record Email(String value) {
} 

public record PhoneNumber(String value) {
}

public record ContactInfo(Email email, PhoneNumber phoneNumber) {
}
```
本題とは関係ないですが、Recordsを使うとvalue Objectの定義はとても簡単です。

Bean ValidationはまだRecordsに対応していませんが、YAVIでは最初のリリースから既に次のようにRecordsに対するValidationを定義できます。

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

public record Email(String value) {
	private static final Validator<Email> validator = ValidatorBuilder.<Email>of()
			.constraint(Email::value, "email",
					c -> c.notBlank().lessThanOrEqual(128).email())
			.build();
}
```

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

public record PhoneNumber(String value) {
	private static final Validator<PhoneNumber> validator = ValidatorBuilder.<PhoneNumber>of()
			.constraint(PhoneNumber::value, "phoneNumber",
					c -> c.notBlank().lessThanOrEqual(16).pattern("[0-9\\-]+"))
			.build();
}
```

YAVI 0.6.0からはValidationの結果として`Validated`型のオブジェクトを返すことができるようになります。これを使って、`Email`と`PhoneNumber`のfactory methodを作成します。

```java
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.Validator;

public record Email(String value) {
	private static final Validator<Email> validator = ValidatorBuilder.<Email>of()
			.constraint(Email::value, "email",
					c -> c.notBlank().lessThanOrEqual(128).email())
			.build();

	public static Validated<Email> of(String value) {
		return validator.applicative().validate(new Email(value));
	}
}
```

```java
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.Validator;

public record PhoneNumber(String value) {
	private static final Validator<PhoneNumber> validator = ValidatorBuilder.<PhoneNumber>of()
			.constraint(PhoneNumber::value, "phoneNumber",
					c -> c.notBlank().lessThanOrEqual(16).pattern("[0-9\\-]+"))
			.build();

	public static Validated<PhoneNumber> of(String value) {
		return validator.applicative().validate(new PhoneNumber(value));
	}
}
```

Value Objectは常に不変条件を満たすべきであり、インスタンス作成時に入力チェック済み状態で返すというのはとても理にかなっています。

`Validated`型は次のように使えます。

```java
Validated<Email> emailValidated = Email.of("maki@example.com");
if (mailValidated.isValid()) {
	Email email = emailValidated.value();
	// ...
} else {
	ConstraintViolations violations = emailValidated.errors();
	// ...
}
```

if文を書かずに、入力チェックにエラーがある場合に例外をスローさせたい場合は、次のように書くことができます。
```java
Email email = emailValidated
	.orElseThrow(violations -> new ConstraintViolationsException(violations));
```

あるいは`fold`メソッドで入力チェックが成功の場合も失敗の場合も同じ型に変換することができます。

```java
HttpStatus status = emailValidated.fold(violations -> HttpStatus.BAD_REQUEST, email -> HttpStatus.OK)
```

> `Validated<T>`型は後述の`Validation<E, T>`型の一部特化型である`Validation<ConstraintViolation, T>`型です。

ここまでは0.6.0より前からサポートしていた`Either`でも同じように書けます。
`Validated`型、あるいはその親クラスである`Validation`型の場合、次のように入力チェック結果の合成及びエラー情報の集約ができます。

```java
Validated<Email> emailValidated = Email.of("maki@example.com");
Validated<PhoneNumber> phoneNumberValidated = PhoneNumber.of("0120-3456-7890");
Validated<ContactInfo> contactInfoValidated = Validations.apply((email, phoneNumber) -> new ContactInfo(email, phoneNumber), emailValidated, phoneNumberValidated)
```
> 説明のため、メソッド参照を使わず明示的にラムダ式を書いています。

`Validations.apply`は次のコードと同じです。

```java
Validated<ContactInfo> contactInfoValidated = Validations.combine(emailValidated, phoneNumberValidated)
	.apply((email, phoneNumber) -> new ContactInfo(email, phoneNumber));
```
または
```java
Validated<ContactInfo> contactInfoValidated = emailValidated.combine(phoneNumberValidated)
	.apply((email, phoneNumber) -> new ContactInfo(email, phoneNumber));
```

> `apply`メソッドの引数は0.6.0時点では10個までサポートされています。

例えば`Email`に` `(空白スペース)、`PhoneNumber`に`a`を入力して、`ContactInfo`を作成しようとすると、
入力チェックは失敗し、次の3つの`ContraintViolation`が返ります。

```
* "email" must not be blank
* "email" must be a valid email address
* "phoneNumber" must match [0-9\-]+
```

`Email`と`PhoneNumber`のエラーメッセージが集約されていることがわかります。

今回はfactory methodで`Validated`型を使用する例を紹介しましたが、通常のフローにおける入力チェックに使ってももちろん良いです。
これまで提供してきた`Either`を使用するよりも`Validated`型を使用する方がオススメです。
`Validated`型は他の`Validated`型オブジェクトと組み合わせ可能なため、Validationロジックの再利用性も高いです。

### 導入の背景

では、なぜこの機能を追加したかというと、契機はこの [issue](https://github.com/making/yavi/issues/119) です。
コード例にKotlinが使われていますが、要約すると

* Value Objectのfactory methodでYAVIによるValidationをかけた状態の値を返したい
* YAVIで提供されている`Either`を返した場合、Value Objectの合成に使用できず、再利用性が低い。

というものでした。このissueでは [konad](https://github.com/lucapiccinelli/konad) とのIntegrationを提案されましたが、Kotlin特化なものを使うわけにはいかなかったため、Javaで実装しました。
やはりfactory methodでの入力チェック済みのオブジェクトを返す用途が増えているようですね。

YAVIは元々ネストしたオブジェクトのValidationはサポートしています。したがって次のような書き方はできます。

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

public record Email(String value) {
	public static final Validator<Email> validator = ValidatorBuilder.<Email>of()
			.constraint(Email::value, "value",
					c -> c.notBlank().lessThanOrEqual(128).email())
			.build();
}
```

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

public record PhoneNumber(String value) {
	public static final Validator<PhoneNumber> validator = ValidatorBuilder.<PhoneNumber>of()
			.constraint(PhoneNumber::value, "value",
					c -> c.notBlank().lessThanOrEqual(16).pattern("[0-9\\-]+"))
			.build();
}
```

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

public record ContactInfo(Email email, PhoneNumber phoneNumber) {
	public static final Validator<ContactInfo> validator = ValidatorBuilder.<ContactInfo>of()
			.nest(ContactInfo::email, "email", Email.validator)
			.nest(ContactInfo::phoneNumber, "phoneNumber", PhoneNumber.validator)
			.build();
}
```

このように`ContactInfo`側にValidatorを定義し、Nestした`Email`と`PhoneNumber`のValidatorに委譲するという使い方を想定してました。

Validationの使い方は次のようになります。

```java
Either<ConstraintViolations, ContactInfo> either = ContactInfo.validator.either()
		.validate(new ContactInfo(new Email(" "), new PhoneNumber("a")));
ContactInfo contactInfo = either.rightOrElseThrow(violations -> new ConstraintViolationsException(violations));
System.out.println(contactInfo);
```

`Validated`型の場合と同じく、入力チェックは失敗し、次の3つの`ContraintViolation`が返ります。

```
* "email.value" must not be blank
* "email.value" must be a valid email address
* "phoneNumber.value" must match [0-9\-]+
```

得られる結果は同じなのですが、"全て組み立ててからValidationする"か、"Validatedなものを使って組み立てるか"の違いがあります。
設計方針によりますが、どちらもユースケースはあると思います。 
前者の場合、例えばWebフレームワークがリクエストパラメータから組み立てたオブジェクトをWeb層でValidationしたい場合に有用です。
後者の場合、不変状態を満たしたドメインオブジェクトを使ってドメイン層の処理を行いたい場合に有用です。

0.6.0より前のバージョンは主に前者の用途を考慮していました。後者の用途としては [Arguments Validator](https://github.com/making/yavi#arguments-validator) という機能があるのですが、ここでは説明を割愛します。

ではfactory methodで`Either`を使って後者のユースケースを実装できないのでしょうか？

次のようにfactory methodを書き直してみます。

```java
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
import am.ik.yavi.fn.Either;

public record Email(String value) {
	private static final Validator<Email> validator = ValidatorBuilder.<Email>of()
			.constraint(Email::value, "email",
					c -> c.notBlank().lessThanOrEqual(128).email())
			.build();

	public static Either<ConstraintViolations, Email> of(String value) {
		return validator.either().validate(new Email(value));
	}
}
```

```java
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
import am.ik.yavi.fn.Either;

public record PhoneNumber(String value) {
	private static final Validator<PhoneNumber> validator = ValidatorBuilder.<PhoneNumber>of()
			.constraint(PhoneNumber::value, "phoneNumber",
					c -> c.notBlank().lessThanOrEqual(16).pattern("[0-9\\-]+"))
			.build();

	public static Either<ConstraintViolations, PhoneNumber> of(String value) {
		return validator.either().validate(new PhoneNumber(value));
	}
}
```

このfactory methodを使って`ContactInfo`を作成すると次のようなコードになります。

```java
Either<ConstraintViolations, Email> emailEither = Email.of("   ");
Either<ConstraintViolations, PhoneNumber> phoneNumberEither = PhoneNumber.of("aaa");

Email email = emailEither.rightOrElseThrow(ConstraintViolationsException::new);
PhoneNumber phoneNumber = phoneNumberEither.rightOrElseThrow(ConstraintViolationsException::new);
ContactInfo contactInfo = new ContactInfo(email, phoneNumber);
```
あるいは0.6.0から追加された`Either.flatMap`メソッドを使って次のように`Either`に合成できます。
```java
Either<ConstraintViolations, ContactInfo> contactInfoEither = emailEither
		.flatMap(email -> phoneNumberEither
				.rightMap(phoneNumber ->
						new ContactInfo(email, phoneNumber)));
ContactInfo contactInfo = contactInfoEither.rightOrElseThrow(ConstraintViolationsException::new);
```
どちらの場合にも、入力チェックは失敗し、次の`ContraintViolation`が返ります。

```
* "email" must not be blank
* "email" must be a valid email address
```

見てわかるように、入力チェックが途中でショートカットされて、`Email`に関する`ContraintViolation`しか取得できません。
エラーを集約するには次のようなコードを書く必要があります。
```java
ConstraintViolations violations = new ConstraintViolations();
Either<ConstraintViolations, ContactInfo> contactInfoEither;
emailEither.peekLeft(violations::addAll);
phoneNumberEither.peekLeft(violations::addAll);
if (violations.isEmpty()) {
	contactInfoEither = Either.right(new ContactInfo(emailEither.right().get(), phoneNumberEither.right().get()));
}
else {
	contactInfoEither = Either.left(violations);
}
ContactInfo contactInfo = contactInfoEither.rightOrElseThrow(ConstraintViolationsException::new);
```
すっきりしないですが、これでようやく次の3つの`ContraintViolation`が返ります。

```
* "email" must not be blank
* "email" must be a valid email address
* "phoneNumber" must match [0-9\-]+
```

既に`Validated`の例を見たので、このコードがよくないことがわかります。

`Either<E, T>`型に対して、エラーをショートカットせず集約するデータ構造が[`Validation<E, T>`](https://github.com/making/yavi/blob/develop/src/main/java/am/ik/yavi/fn/Validation.java)型です。
[`Validated<T>`](https://github.com/making/yavi/blob/develop/src/main/java/am/ik/yavi/core/Validated.java)は汎用型である`Validation<E, T>`型の`E = ConstraintViolation`である特化型です。

> YAVIではSemigroupの概念は導入せず、シンプルに`List<E>`でエラーを集約しました。

HaskellやScalazなどの関数型プログラミングの [Applicative Functor](https://en.wikipedia.org/wiki/Applicative_functor) の考え方を使っています。
このようなValidationはデザインパターンとして色々な箇所で使われているので、知っておくと便利かもしれません。
> 例えば [Micrometer](https://github.com/micrometer-metrics/micrometer/blob/main/micrometer-core/src/main/java/io/micrometer/core/instrument/config/validate/Validated.java) で使用されています。

YAVIは実装として[vavr](https://www.vavr.io)を参考にしました。また、[gakuzzzz](https://twitter.com/gakuzzzz)にたくさんアドバイスをいただきました。ありがとうございます。

---

YAVIはValidationライブラリとして、色々なプログラミングパターンを採用しつつも、業務で使える実用的なバリデーションルールを備えています。
Starも430以上付いて、利用が増えてきています。まだエッジケースでバグがありますが、ぜひ使ってみて[フィードバック](https://github.com/making/yavi/issues)をください。
