---
title: YAVIによるValue ObjectのValidation
tags: ["Java", "YAVI"]
categories: ["Programming", "Java", "am", "ik", "yavi"]
date: 2022-06-10T12:45:10Z
updated: 2022-06-10T12:50:56Z
---

[YAVI](https://github.com/making/yavi)には[`ValueValidator<X, Y>`](https://yavi.ik.am/#define-validator-for-a-single-value)という、あるXクラスの値を検証した後にYクラスの値に変換して返す便利な機能があります。
この機能を使うとValue ObjectのValidationをエレガントに定義できます。

次のRecordを題材にします。

```java
record Name(String value) { }
record Age(Integer value) { }
record Person(Name name, Age age) { }
```

`String`を検証し、`Name`に変換して返す`ValueValidator<String, Name>`や`Integer`を検証して`Age`を返す`ValueValidator<Integer, Age>`は次のように定義できます。

```java
final StringValidator<Name> nameValidator = StringValidatorBuilder
		.of("name", c -> c.notBlank().lessThanOrEqual(255))
		.build(Name::new);

final IntegerValidator<Age> ageValidator = IntegerValidatorBuilder
		.of("age", c -> c.notNull().greaterThanOrEqual(0))
		.build(Age::new);
```

検証結果は次のように`Validated`型として返ります。

```java
final Validated<Name> nameValidated = nameValidator.validate("John Doe");
final Validated<Age> ageValidated = ageValidator.validate(30);
```

検証が成功する場合は変換されたインスタンスを取得し、検証が失敗する場合は例外をスローしたい場合は次のように取り出せます。

```java
final Name name = nameValidated.orElseThrow(ConstraintViolationsException::new);
final Age age = ageValidated.orElseThrow(ConstraintViolationsException::new);
```

ショートカットしたい場合は、次のように書けます。

```java
final Name name = nameValidator.validated("John Doe");
final Age age = ageValidator.validated(30);
```

`Validated<Name>`と`Validated<Age>`から`Validated<Person>`を作ることができます。

```java
final Validated<Person> personValidated = nameValidated.combine(ageValidated).apply(Person::new);
```

この場合、`Name`と`Age`のエラーメッセージは集約されます。

次のようなファクトリメソッドを作ると便利でしょう。

```java
record Name(String value) {
	static final StringValidator<Name> validator = StringValidatorBuilder
			.of("name", c -> c.notBlank().lessThanOrEqual(255))
			.build(Name::new);

	public static Validated<Name> of(String value) {
		return validator.validate(value);
	}
}

record Age(Integer value) {
	static final IntegerValidator<Age> validator = IntegerValidatorBuilder
			.of("age", c -> c.notNull().greaterThanOrEqual(0))
			.build(Age::new);

	public static Validated<Age> of(Integer value) {
		return validator.validate(value);
	}
}
```

次のように利用できます。

```java
final Validated<Person> personValidated = Name.of("John Doe")
		.combine(Age.of(30))
		.apply(Person::new);
```

recordの場合はパブリックコンストラクタができるため、`of`ファクトリを用意しても、入力チェックなしでコンストラクタからインスタンスを生成することができてしまいます。
コンストラクタ内で入力チェックし、不変条件を満たさないインスタンスを作れないようにしたい場合は、`lazy()`メソッドをつけることでコンストラクタ内でvalidateを実行可能です。

```java
record Name(String value) {
	static final StringValidator<Name> validator = StringValidatorBuilder
			.of("name", c -> c.notBlank().lessThanOrEqual(255))
			.build(Name::new);
	
	// compact constructor
	Name {
		Name.validator.lazy().validated(value);
	}
}

record Age(Integer value) {
	static final IntegerValidator<Age> validator = IntegerValidatorBuilder
			.of("age", c -> c.notNull().greaterThanOrEqual(0))
			.build(Age::new);

	// compact constructor
	Age {
		Age.validator.lazy().validated(value);
	}
}
```

`lazy()`がないと、例えば`Name`コンストラクタ内で`Name`インスタンスを作ろうとしてまた`Name`コンストラクタが呼ばれ、`StackOverflowError`が発生します。
`lazy()`をつけると`Name`インスタンスを返す代わりに`Supplier<Name>`が返り、インスタンスを生成するタイミングを遅延できます。これにより`StackOverflowError`の発生が防がれます。

```java
Name name = new Name("  "); // => "name" must not be blank
Age age = new Age(-1); // => "age" must be greater than or equal to 0
```

`Validated<Name>`と`Validated<Age>`から`Validated<Person>`を作る代わりに、
`StringValidator<Name>`と`IntegerValidator<Age>`から`Arguments2Validator<String, Integer, Person>`を作り、
そこから直接`Validated<Person>`を作ることもできます。

```java
final Arguments2Validator<String, Integer, Person> personValidator = Name.validator
		.split(Age.validator)
		.apply(Person::new);
```

次のように利用できます。

```java
final Validated<Person> personValidated = Person.validator.validate("John Doe", 30);
```
または
```java
final Person person = Person.validator.validated("John Doe", 30);
```

エラーは集約されるので、次のような検証を行うと、
```java
final Person person = Person.validator.validated("  ", -1);
```
次のようなメッセージを持つ例外がスローされます。
```
am.ik.yavi.core.ConstraintViolationsException: Constraint violations found!
* "name" must not be blank
* "age" must be greater than or equal to 0
```

なお、制約違反情報は`ConstraintViolationsException#violations`で取得できます。

---

YAVIを使ったValue ObjectのValidationについて紹介しました。
とても便利なので[YAVI](https://github.com/making/yavi)を使いましょう。
