YAVIとは?
- Java のバリデーションライブラリ
- ラムダを使用し、型安全な API を提供する
- Yet Another Validation for Java の略で、読みはヤバイ
- 日本人の @making さんが作っている
- Spring WebFlux.fn で使えるバリデーションが欲しいというのが動機で、 Bean Validation を置き換えるのが目的ではない
- 詳しい話は Java用Validatorライブラリ"YAVI"(ヤヴァイ)の紹介 - IK.AM を参照のこと
環境
>gradle --version
------------------------------------------------------------
Gradle 8.8
------------------------------------------------------------
Build time: 2024-05-31 21:46:56 UTC
Revision: 4bd1b3d3fc3f31db5a26eecb416a165b8cc36082
Kotlin: 1.9.22
Groovy: 3.0.21
Ant: Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM: 21.0.3 (Eclipse Adoptium 21.0.3+9-LTS)
OS: Windows 10 10.0 amd64
Hello World
実装
build.gradle
plugins {
id "java"
}
java {
sourceCompatibility = 21
targetCompatibility = 21
}
compileJava.options.encoding = "UTF-8"
repositories {
mavenCentral()
}
dependencies {
implementation "am.ik.yavi:yavi:0.14.0"
}
Hoge.java
package sandbox.yavi;
public record Hoge(String text, int number) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.notBlank())
.constraint(Hoge::number, "number",
c -> c.greaterThan(0).lessThanOrEqual(100))
.build();
printResult(validator, new Hoge("", 1));
printResult(validator, new Hoge("a", 101));
printResult(validator, new Hoge("a", 100));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
if (!violations.isValid()) {
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
}
実行結果
Hoge[text=, number=1] > invalid
"text" must not be blank
Hoge[text=a, number=101] > invalid
"number" must be less than or equal to 100
Hoge[text=a, number=100] > valid
説明
build.gradle
dependencies {
implementation "am.ik.yavi:yavi:0.14.0"
}
- 依存関係として am.ik.yavi:yavi を設定する
Main.java
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.notBlank())
.constraint(Hoge::number, "number",
c -> c.greaterThan(0).lessThanOrEqual(100))
.build();
-
ValidatorBuilder
を使ってValidator
を作成する -
of
メソッドの型引数で、バリデーション対象の型を指定する -
constraint
メソッドで、バリデーション対象の型に対する制約を定義する- 第一引数に、オブジェクトから検査対象の値を取得するための関数を渡す
- 多くの場合、これは Getter のメソッド参照を指定することで記述できる
- 第二引数で、検査対象の項目名を指定する
- 第三引数には、具体的な制約の内容を定義する関数を渡す
- ここでは
text
が空でないことと、number
が 0 より大きく 100 以下であることを定義している
- ここでは
- 第一引数に、オブジェクトから検査対象の値を取得するための関数を渡す
Main.java
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
if (!violations.isValid()) {
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
-
Validator
のvalidate
メソッドに検査対象のオブジェクトを渡すことで、違反情報が格納されたConstraintViolations
が返される -
isValid
メソッドで全体のバリデーション結果が分かる -
ConstraintViolations
はConstraintViolation
のList
となっており、個々の違反結果の詳細はConstraintViolation
から取得できる
基本
Validator
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.notBlank())
.constraint(Hoge::number, "number",
c -> c.greaterThan(0).lessThanOrEqual(100))
.build();
...
final ConstraintViolations violations = validator.validate(target);
- YAVI では、検査したいクラスごとに
Validator
を用意する-
Validator
のインスタンスはスレッドセーフで、様々な場所で再利用できるように設計されている
-
-
Validator
のインスタンスは、ValidatorBuilder
を使って作成する-
of
メソッドでビルダーの作成を開始する - このとき、型引数で検査対象のクラスを指定する
-
-
ValidatorBuilder
には制約を定義するためのconstraint
メソッドが用意されている- 第一引数には、対象オブジェクトから検査対象の値を取得するための関数を渡す
- 通常、ここには検査対象の値を取得するための Getter をメソッド参照で指定すればいい
- 第二引数には、検査対象の値の名前を指定する
- この名前は、エラーメッセージに利用される
- 後述するアノテーションプロセッサーを利用すると、名前を文字列で指定せずに実装することもできるようになる
- 第三引数には、具体的な制約の内容を定義する関数を渡す
- この関数には、制約を定義するためのオブジェクト(
Constraint
およびそのサブタイプ)が渡される -
notNull
やgreaterThan
など、制約を定義するためのメソッドが多数用意されていて、これらをメソッドチェーンで繋げることによって制約を定義する
- この関数には、制約を定義するためのオブジェクト(
- 第一引数には、対象オブジェクトから検査対象の値を取得するための関数を渡す
-
Validator
のvalidate
メソッドに検査対象のオブジェクトを渡すことで、検査を実行できる
ConstraintViolations, ConstraintViolation
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
if (!violations.isValid()) {
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
-
Validator
のvalidate
で検索を実行すると、違反情報がConstraintViolations
という型で返される -
ConstraintViolations
はConstraintViolation
のList
となっていて、ConstraintViolation
に個々の違反内容が格納されている- 違反が1つ以上あるかどうかは
isValid
メソッドで確認できる - 違反がゼロの場合は、空になっている
- 違反が1つ以上あるかどうかは
-
ConstraintViolation
には、以下のような個々の違反の詳細情報が格納されている- エラーになった項目の名前 (
name
メソッド) - エラーメッセージ (
message
メソッド) - エラーになった値 (
violatedValue
メソッド)
- エラーになった項目の名前 (
違反情報をシリアライズしやすい形にする
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
import am.ik.yavi.core.ViolationDetail;
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.notBlank())
.constraint(Hoge::number, "number",
c -> c.greaterThan(0).lessThanOrEqual(100))
.build();
final ConstraintViolations violations = validator.validate(new Hoge("", 101));
final List<ViolationDetail> details = violations.details();
details.forEach(detail -> System.out.printf("""
ViolationDetail {
key = %s
args = %s
defaultMessage = %s
}
""",
detail.getKey(),
Arrays.toString(detail.getArgs()),
detail.getDefaultMessage()
));
}
}
実行結果
ViolationDetail {
key = charSequence.notBlank
args = [text, ]
defaultMessage = "text" must not be blank
}
ViolationDetail {
key = numeric.lessThanOrEqual
args = [number, 100, 101]
defaultMessage = "number" must be less than or equal to 100
}
-
ConstraintViolations
のdetails
またはConstraintViolation
のdetail
メソッドを使用すると、違反情報をViolationDetail
に変換できる -
ConstraintViolation
は内部にMessageFormatter
を持っているなど、そのままシリアライズするには都合が悪い構造になっている -
ViolationDetail
はただのデータオブジェクトなので、そのままシリアライズしやすくなっている - Jackson で JSON にシリアライズしたい、といった場合などに使える
null 値の扱い
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
// ★ 「text は 10 文字未満」という制約を定義
.constraint(Hoge::text, "text", c -> c.lessThan(10))
.constraint(Hoge::number, "number", c -> c.greaterThan(0))
.build();
// ★ text に null を設定
printResult(validator, new Hoge(null, 1));
printResult(validator, new Hoge("a", 1));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
if (!violations.isValid()) {
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
}
実行結果
Hoge[text=null, number=1] > valid
Hoge[text=a, number=1] > valid
- null は基本的に違反にはならない
- null を違反としたい場合は、
notNull
やnotEmpty
,notBlank
などで明示的に null が違反であることを定義する
フェイルファースト
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.notBlank())
.constraint(Hoge::number, "number",
c -> c.greaterThan(0).lessThanOrEqual(100))
.build();
final Hoge hoge = new Hoge("", -1); // ★ 複数の項目が違反になるように定義
final ConstraintViolations violations = validator.validate(hoge);
System.out.println("valid=" + violations.isValid());
for (ConstraintViolation violation : violations) {
System.out.println("message=" + violation.message());
}
}
}
実行結果
valid=false
message="text" must not be blank
message="number" must be greater than 0
- デフォルトでは、途中で違反が見つかったとしても全ての項目に対して検査が実施される
- 違反が見つかった場合、その時点で検査を中断させたい場合はフェイルファーストモードを有効にする
フェイルファーストモードを有効にした場合
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.notBlank())
.constraint(Hoge::number, "number",
c -> c.greaterThan(0).lessThanOrEqual(100))
.failFast(true) // ★ failFast で true を設定する
.build();
final Hoge hoge = new Hoge("", -1);
final ConstraintViolations violations = validator.validate(hoge);
System.out.println("valid=" + violations.isValid());
for (ConstraintViolation violation : violations) {
System.out.println("message=" + violation.message());
}
}
}
実行結果
valid=false
message="text" must not be blank
-
Validator
を作成するときにfailFast(true)
を指定することで、Validator
をフェイルファーストモードに変更できる - フェイルファーストモードの
Validator
は、違反が見つかるとその時点で検査を中断するようになる
既存の Validator をフェイルファーストモードに切り替える
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.notBlank())
.constraint(Hoge::number, "number",
c -> c.greaterThan(0).lessThanOrEqual(100))
.build();
final Validator<Hoge> failFastValidator = validator.failFast(true);
final Hoge hoge = new Hoge("", -1);
System.out.println("=== Original validator ===");
validate(validator, hoge);
System.out.println("=== Fail fast validator ===");
validate(failFastValidator, hoge);
}
private static void validate(Validator<Hoge> validator, Hoge hoge) {
final ConstraintViolations violations = validator.validate(hoge);
System.out.println("valid=" + violations.isValid());
for (ConstraintViolation violation : violations) {
System.out.println("message=" + violation.message());
}
}
}
実行結果
=== Original validator ===
valid=false
message="text" must not be blank
message="number" must be greater than 0
=== Fail fast validator ===
valid=false
message="text" must not be blank
-
Validator
のfailFast
メソッドを使うことで、既存のValidator
をフェイルファーストモードに切り替えた新しいValidator
を取得できる
ネストされたオブジェクトの制約
NestedClass.java
package sandbox.yavi;
public record NestedClass(int number, String text) {
}
EnclosingClass.java
package sandbox.yavi;
public record EnclosingClass(String text, NestedClass nested) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final Validator<NestedClass> nestedValidator
= ValidatorBuilder.<NestedClass>of()
.constraint(NestedClass::number, "number", c -> c.greaterThan(0))
.constraint(NestedClass::text, "text", c -> c.notBlank())
.build();
final Validator<EnclosingClass> encloseingValidator
= ValidatorBuilder.<EnclosingClass>of()
.constraint(EnclosingClass::text, "text", c -> c.notBlank())
.nest(EnclosingClass::nested, "nested", nestedValidator)
.build();
printResult(encloseingValidator,
new EnclosingClass("enclosing", new NestedClass(-1, "nested")));
printResult(encloseingValidator,
new EnclosingClass("enclosing", new NestedClass(1, "nested")));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
EnclosingClass[text=enclosing, nested=NestedClass[number=-1, text=nested]] > invalid
"nested.number" must be greater than 0
EnclosingClass[text=enclosing, nested=NestedClass[number=1, text=nested]] > valid
説明
Main.java
final Validator<NestedClass> nestedValidator
= ValidatorBuilder.<NestedClass>of()
.constraint(NestedClass::number, "number", c -> c.greaterThan(0))
.constraint(NestedClass::text, "text", c -> c.notBlank())
.build();
final Validator<EnclosingClass> encloseingValidator
= ValidatorBuilder.<EnclosingClass>of()
.constraint(EnclosingClass::text, "text", c -> c.notBlank())
.nest(EnclosingClass::nested, "nested", nestedValidator)
.build();
- ネストされたオブジェクトの制約を定義する場合は、
nest
メソッドを使用する - ネストされたクラス自体の
Validator
は別途定義しておき、nest
メソッドの第三引数で渡す
その場で制約定義する
public class Main {
public static void main(String[] args) {
final Validator<EnclosingClass> encloseingValidator
= ValidatorBuilder.<EnclosingClass>of()
.constraint(EnclosingClass::text, "text", c -> c.notBlank())
.nest(EnclosingClass::nested, "nested",
// ★その場で制約を定義
b -> b.constraint(NestedClass::number, "number",
c -> c.greaterThan(0))
.constraint(NestedClass::text, "text",
c -> c.notBlank())
)
.build();
printResult(encloseingValidator,
new EnclosingClass("enclosing", new NestedClass(-1, "nested")));
printResult(encloseingValidator,
new EnclosingClass("enclosing", new NestedClass(1, "nested")));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
EnclosingClass[text=enclosing, nested=NestedClass[number=-1, text=nested]] > invalid
"nested.number" must be greater than 0
EnclosingClass[text=enclosing, nested=NestedClass[number=1, text=nested]] > valid
-
nest
メソッドの第三引数に、ネストされたクラスの制約を定義する関数を渡すことで、事前に定義したValidator
ではなくその場で制約を定義することもできる
ネストされた項目を null 可にする
何もしない場合のデフォルトの動き
public class Main {
public static void main(String[] args) {
final Validator<NestedClass> nestedValidator
= ValidatorBuilder.<NestedClass>of()
.constraint(NestedClass::number, "number", c -> c.greaterThan(0))
.constraint(NestedClass::text, "text", c -> c.notBlank())
.build();
final Validator<EnclosingClass> encloseingValidator
= ValidatorBuilder.<EnclosingClass>of()
.constraint(EnclosingClass::text, "text", c -> c.notBlank())
.nest(EnclosingClass::nested, "nested", nestedValidator)
.build();
// nested に null を設定
printResult(encloseingValidator,
new EnclosingClass("enclosing", null));
printResult(encloseingValidator,
new EnclosingClass("enclosing", new NestedClass(1, "nested")));
}
}
実行結果
EnclosingClass[text=enclosing, nested=null] > invalid
"nested" must not be null
EnclosingClass[text=enclosing, nested=NestedClass[number=1, text=nested]] > valid
-
nest
で定義した項目は、 null 不可扱いになる - null 可としたい場合は
nestIfPresent
を使用する
nestIfPresentを使った場合
public class Main {
public static void main(String[] args) {
final Validator<NestedClass> nestedValidator
= ValidatorBuilder.<NestedClass>of()
.constraint(NestedClass::number, "number", c -> c.greaterThan(0))
.constraint(NestedClass::text, "text", c -> c.notBlank())
.build();
final Validator<EnclosingClass> encloseingValidator
= ValidatorBuilder.<EnclosingClass>of()
.constraint(EnclosingClass::text, "text", c -> c.notBlank())
// ★ nestIfPresent を使用
.nestIfPresent(EnclosingClass::nested, "nested", nestedValidator)
.build();
final EnclosingClass enclosing = new EnclosingClass("", null);
final ConstraintViolations violations = encloseingValidator.validate(enclosing);
violations.stream()
.map(ConstraintViolation::message)
.forEach(System.out::println);
}
}
実行結果
EnclosingClass[text=enclosing, nested=null] > valid
EnclosingClass[text=enclosing, nested=NestedClass[number=1, text=nested]] > valid
コレクション・配列の制約
List
Hoge.java
package sandbox.yavi;
public record Hoge(String text) {
}
Hoges.java
package sandbox.yavi;
import java.util.List;
public record Hoges(List<Hoge> list) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
import java.util.List;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> hogeValidator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.notEmpty())
.build();
final Validator<Hoges> hogesValidator
= ValidatorBuilder.<Hoges>of()
.forEach(Hoges::list, "list", hogeValidator)
.build();
printResult(hogesValidator, new Hoges(List.of(
new Hoge("a"),
new Hoge("b"),
new Hoge("")
)));
printResult(hogesValidator, new Hoges(List.of(
new Hoge("a"),
new Hoge("b"),
new Hoge("c")
)));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
Hoges[list=[Hoge[text=a], Hoge[text=b], Hoge[text=]]] > invalid
"list[2].text" must not be empty
Hoges[list=[Hoge[text=a], Hoge[text=b], Hoge[text=c]]] > valid
-
List
やSet
、配列の制約を定義する場合は、forEach
を使用する
.forEach(Hoges::list, "list", hogeValidator)
-
forEach
の第三引数には、コレクション要素のクラス用のValidator
を渡す - もしくは、その場で要素用の制約を定義することもできる
その場で制約を定義する場合
public static void main(String[] args) {
final Validator<Hoges> hogesValidator
= ValidatorBuilder.<Hoges>of()
.forEach(Hoges::list, "list",
// ★この場で Hoge の制約を定義する
b -> b.constraint(Hoge::text, "text", c -> c.notEmpty()))
.build();
printResult(hogesValidator, new Hoges(List.of(
new Hoge("a"),
new Hoge("b"),
new Hoge("")
)));
printResult(hogesValidator, new Hoges(List.of(
new Hoge("a"),
new Hoge("b"),
new Hoge("c")
)));
}
String やラッパークラスのコレクションの制約をスマートに定義する方法は用意されていない
Hoge.java
package sandbox.yavi;
import java.util.List;
public record Hoge(List<String> textList, List<Integer> numberList) {
}
- こんな感じで、
String
やInteger
などのラッパークラスのコレクションを持つ項目に対して制約を定義する場合を考える - 無理やり書くと、以下のようになる
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
import java.util.List;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.forEach(Hoge::textList, "textList",
b -> b._string(s -> s, "", c -> c.notEmpty()))
.forEach(Hoge::numberList, "numberList",
b -> b._integer(i -> i, "", c -> c.lessThan(50)))
.build();
printResult(validator, new Hoge(List.of ("a", "b", ""), List.of(1, 10, 100)));
printResult(validator, new Hoge(List.of ("a", "b", "c"), List.of(1, 10, 20)));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
Hoge[textList=[a, b, ], numberList=[1, 10, 100]] > invalid
"textList[2]" must not be empty
"numberList[2]" must be less than 50
Hoge[textList=[a, b, c], numberList=[1, 10, 20]] > valid
-
_string(s -> s, "", ...)
のようにして同じ値をそのまま渡すための記述が必要であったり、項目名を空文字にしてエラーメッセージが整合するようにしていて、やや無理やり感がある -
String
やラッパークラスのコレクションに対する制約がこのような書き方になることについて Issue が建てられており、以下のような回答がある
That's expected
(訳)これは想定通りです。forEach does not work sensibly with primitive list · Issue #301 · making/yavi
- ということで、このような書き方になるのは仕様とのこと
- 特に説明がないが「
String
やプリミティブ型は必ず Value Object でラップすべき」という思想(前提)があるということだろうか?
Map
Hoge.java
package sandbox.yavi;
public record Hoge(String text) {
}
Hoges.java
package sandbox.yavi;
import java.util.Map;
public record Hoges(Map<String, Hoge> map) {
}
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
import java.util.Map;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> hogeValidator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.notEmpty())
.build();
final Validator<Hoges> hogesValidator = ValidatorBuilder.<Hoges>of()
.forEach(Hoges::map, "map", hogeValidator)
.build();
printResult(hogesValidator, new Hoges(Map.of(
"a", new Hoge("A"),
"b", new Hoge("B"),
"c", new Hoge("")
)));
printResult(hogesValidator, new Hoges(Map.of(
"a", new Hoge("A"),
"b", new Hoge("B"),
"c", new Hoge("C")
)));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
Hoges[map={a=Hoge[text=A], b=Hoge[text=B], c=Hoge[text=]}] > invalid
"map[2].text" must not be empty
Hoges[map={a=Hoge[text=A], b=Hoge[text=B], c=Hoge[text=C]}] > valid
-
Map
も、他のコレクションと同様にforEach
を使って制約を定義できる - ただし、制約の検査は
Map
の値に対して行われるものになる - もし
Map
のキーに対して制約を定義したい場合は、keySet
でキーのSet
を取り出して制約を定義する必要がある
Mapのキーに対して制約を定義する
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
import java.util.Map;
public class Main {
public static void main(String[] args) {
final Validator<Hoges> hogesValidator = ValidatorBuilder.<Hoges>of()
// ★ keySet を取り出す形にする
.forEach((Hoges hoges) -> hoges.map().keySet(), "map",
b -> b._string(s -> s, "key", c -> c.notEmpty()))
.build();
printResult(hogesValidator, new Hoges(Map.of(
"a", new Hoge("A"),
"", new Hoge("B"),
"c", new Hoge("C")
)));
printResult(hogesValidator, new Hoges(Map.of(
"a", new Hoge("A"),
"b", new Hoge("B"),
"c", new Hoge("C")
)));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
Hoges[map={c=Hoge[text=C], a=Hoge[text=A], =Hoge[text=B]}] > invalid
"map[2].key" must not be empty
Hoges[map={c=Hoge[text=C], b=Hoge[text=B], a=Hoge[text=A]}] > valid
カスタムの制約を定義する
Hoge.java
package sandbox.yavi;
public record Hoge(String text) {
}
MyConstraint.java
package sandbox.yavi;
import am.ik.yavi.core.CustomConstraint;
public class MyConstraint implements CustomConstraint<String> {
@Override
public boolean test(String s) {
return s.startsWith("H");
}
@Override
public String defaultMessageFormat() {
return "\"{0}\" must be start with \"H\" but it is \"{1}\"";
}
@Override
public String messageKey() {
return "my.constraint";
}
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final MyConstraint myConstraint = new MyConstraint();
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.predicate(myConstraint))
.build();
printResult(validator, new Hoge("Hoge"));
printResult(validator, new Hoge("Fuga"));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
Hoge[text=Hoge] > valid
Hoge[text=Fuga] > invalid
"text" must be start with "H" but it is "Fuga"
説明
MyConstraint.java
import am.ik.yavi.core.CustomConstraint;
public class MyConstraint implements CustomConstraint<String> {
- カスタムの制約を定義するには、
CustomConstraint
インタフェースを実装したクラスを作成する
MyConstraint.java
@Override
public boolean test(String s) {
return s.startsWith("H");
}
@Override
public String defaultMessageFormat() {
return "\"{0}\" must be start with \"H\" but it is \"{1}\"";
}
@Override
public String messageKey() {
return "my.constraint";
}
-
test
メソッドには検査対象の値が渡されるので、検査結果をboolean
で返却する- 検査 OK なら
true
を返す
- 検査 OK なら
-
defaultMessageFormat
メソッドでは、デフォルトのメッセージフォーマットを返すように実装する- 埋め込まれるパラメータの先頭は、検査対象の項目名が埋め込まれる
- 埋め込まれるパラメータの末尾は、検査対象の実際の値が埋め込まれる
- デフォルトで埋め込まれるパラメータは「項目名」と「実際の値」の2つなので、
{0}
が項目名になって{1}
が実際の値になる
-
messageKey
メソッドでは、メッセージのキーを返すように実装する- メッセージの取り扱いについての詳細は後述
Main.java
final MyConstraint myConstraint = new MyConstraint();
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.predicate(myConstraint))
.build();
- 自作の
CustomConstraint
は、制約を定義するときにpredicate
メソッドの引数に渡すことで使用できる
メッセージに埋め込むパラメータを追加する
MyConstraint.java
package sandbox.yavi;
import am.ik.yavi.core.CustomConstraint;
public class MyConstraint implements CustomConstraint<String> {
@Override
public boolean test(String s) {
return s.startsWith("H");
}
@Override
public Object[] arguments(String violatedValue) {
return new Object[] { "Foo", "Bar" };
}
@Override
public String defaultMessageFormat() {
return "0={0}, 1={1}, 2={2}, 3={3}, 4={4}";
}
@Override
public String messageKey() {
return "my.constraint";
}
}
実行結果
Hoge[text=Hoge] > valid
Hoge[text=Fuga] > invalid
0=text, 1=Foo, 2=Bar, 3=Fuga, 4={4}
-
arguments
メソッドをオーバーライドすることで、メッセージに埋め込むパラメータを追加できる - 先頭の
{0}
は項目名、末尾の{3}
は実際の値で変わらない -
arguments
が返したパラメータは、これらの間に差し込まれた形でメッセージに埋め込まれる({1}
と{2}
)
複数項目をまたがる制約
Hoge.java
package sandbox.yavi;
public record Hoge(int from, int to) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraintOnTarget(hoge -> hoge.from() < hoge.to(),
"to", "to.isGreaterThanFrom",
"\"to\" must be greater than \"from\"")
.build();
printResult(validator, new Hoge(10, 1));
printResult(validator, new Hoge(1, 10));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
Hoge[from=10, to=1] > invalid
"to" must be greater than "from"
Hoge[from=1, to=10] > valid
-
constraintOnTarget
を使用すると、複数項目にまたがった制約を定義できる - 第一引数に、満たされるべき制約を検証する関数を渡す
- 関数には検査対象のオブジェクトが渡される
- 検査が OK かどうかを boolean で返す(検査 OK なら
true
を返す)
- 第二引数には、項目の名前を渡す
- 第三引数には、メッセージのキーを渡す
- 第四引数には、デフォルトのメッセージを渡す
カスタムの制約を渡す
MyConstraint.java
package sandbox.yavi;
import am.ik.yavi.core.CustomConstraint;
public class MyConstraint implements CustomConstraint<Hoge> {
@Override
public boolean test(Hoge hoge) {
return hoge.from() < hoge.to();
}
@Override
public String defaultMessageFormat() {
return "\"to\" must be greater than \"from\"";
}
@Override
public String messageKey() {
return "my.constraint";
}
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final MyConstraint myConstraint = new MyConstraint();
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
// ★ カスタムの制約を指定する
.constraintOnTarget(myConstraint, "to")
.build();
printResult(validator, new Hoge(10, 1));
printResult(validator, new Hoge(1, 10));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
Hoge[from=10, to=1] > invalid
"to" must be greater than "from"
Hoge[from=1, to=10] > valid
-
constraintOnTarget
の第一引数にカスタムの制約を渡すことができる
特定の条件のときだけ制約を有効にする
Hoge.java
package sandbox.yavi;
public record Hoge(boolean bool, String text) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraintOnCondition((hoge, group) -> hoge.bool(),
b -> b.constraint(Hoge::text, "text", c -> c.notEmpty()))
.build();
printResult(validator, new Hoge(false, ""));
printResult(validator, new Hoge(true, ""));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
Hoge[bool=false, text=] > valid
Hoge[bool=true, text=] > invalid
"text" must not be empty
-
constraintOnCondition
を使うことで、特定の条件が満たされたときだけ検証する制約を定義できる - 第一引数の関数が
true
を返したときだけ、第二引数で定義した制約が有功になる
制約をグルーピングする
Hoge.java
package sandbox.yavi;
public record Hoge(String hoge, String fuga, String piyo) {
}
Group.java
package sandbox.yavi;
import am.ik.yavi.core.ConstraintGroup;
public enum Group implements ConstraintGroup {
HOGE,
FUGA,
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraintOnGroup(Group.HOGE,
b -> b.constraint(Hoge::hoge, "hoge", c -> c.notEmpty()))
.constraintOnGroup(Group.FUGA,
b -> b.constraint(Hoge::fuga, "fuga", c -> c.notEmpty()))
.constraint(Hoge::piyo, "piyo", c -> c.notEmpty())
.build();
final Hoge hoge = new Hoge("", "", "");
printViolations(validator.validate(hoge, Group.HOGE));
printViolations(validator.validate(hoge, Group.FUGA));
printViolations(validator.validate(hoge));
}
private static void printViolations(ConstraintViolations violations) {
System.out.println("valid=" + violations.isValid());
violations.stream()
.map(ConstraintViolation::message)
.forEach(System.out::println);
}
}
実行結果
valid=false
"piyo" must not be empty
"hoge" must not be empty
valid=false
"piyo" must not be empty
"fuga" must not be empty
valid=false
"piyo" must not be empty
説明
Group.java
import am.ik.yavi.core.ConstraintGroup;
public enum Group implements ConstraintGroup {
HOGE,
FUGA,
}
- グループを定義するために、
ConstraintGroup
インタフェースを実装したenum
を定義する
Main.java
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraintOnGroup(Group.HOGE,
b -> b.constraint(Hoge::hoge, "hoge", c -> c.notEmpty()))
.constraintOnGroup(Group.FUGA,
b -> b.constraint(Hoge::fuga, "fuga", c -> c.notEmpty()))
.constraint(Hoge::piyo, "piyo", c -> c.notEmpty())
.build();
-
constraintOnGroup
で、グループを指定して制約を定義できる- 第一引数に、先ほど定義したグループを表す
enum
を渡す - 第二引数で制約を定義する
- 第一引数に、先ほど定義したグループを表す
- グループを指定していない制約は、DEFAULT グループに所属しているものとして扱われる
Main.java
printViolations(validator.validate(hoge, Group.HOGE));
printViolations(validator.validate(hoge, Group.FUGA));
printViolations(validator.validate(hoge));
-
validate
メソッドの第二引数でグループを指定する - グループを指定して検証を行うと、そのグループか DEFAULT グループに所属している制約が検証される
- グループを指定せずに検証を行うと、 DEFAULT グループに所属している制約だけが検証される
validate時のグループ指定 | |||
---|---|---|---|
指定あり | 指定なし | ||
制約定義時の グループ指定 |
指定あり | ○ | × |
指定なし | ○ | ○ |
※○は検証が実施される
違反メッセージを上書きする
Hoge.java
package sandbox.yavi;
public record Hoge(String text) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text",
c -> c.greaterThanOrEqual(5).message("{0} は5文字以上です")
.startsWith("H").message("{0} は H 始まりです"))
.build();
printResult(validator, new Hoge("hoge"));
printResult(validator, new Hoge("Hoge"));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
Hoge[text=hoge] > invalid
text は5文字以上です
text は H 始まりです
Hoge[text=Hoge] > invalid
text は5文字以上です
- 制約を定義した直後に
message
メソッドを続けることで、デフォルトのメッセージを上書きできる
違反メッセージの構築をカスタマイズする
Hoge.java
package sandbox.yavi;
public record Hoge(String text) {
}
MyMessageFormatter.java
package sandbox.yavi;
import am.ik.yavi.message.MessageFormatter;
import java.util.Locale;
public class MyMessageFormatter implements MessageFormatter {
@Override
public String format(
String messageKey, String defaultMessageFormat, Object[] args, Locale locale) {
return "めっせーじ";
}
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.messageFormatter(new MyMessageFormatter())
.constraint(Hoge::text, "text", c -> c.notEmpty())
.build();
printResult(validator, new Hoge(""));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
実行結果
Hoge[text=] > invalid
めっせーじ
説明
MyMessageFormatter.java
public class MyMessageFormatter implements MessageFormatter {
- 違反メッセージの構築をカスタマイズするには、
MessageFormatter
インタフェースを実装したクラスを作成する
MyMessageFormatter.java
@Override
public String format(
String messageKey, String defaultMessageFormat, Object[] args, Locale locale) {
return "めっせーじ";
}
-
format
メソッドをオーバーライドして、違反メッセージを返却するように実装する - 引数として、以下の情報が渡される
-
messageKey
: メッセージを識別するキー -
defaultMessageFormat
: デフォルトのメッセージ -
args
: メッセージに埋め込むパラメータ- 先頭は項目名、末尾は検査対象の実際の値
-
locale
: ロケール
-
Main.java
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.messageFormatter(new MyMessageFormatter())
.constraint(Hoge::text, "text", c -> c.notEmpty())
.build();
- 作成した自作の
MessageFormatter
は、Validator
を作成するときにmessageFormatter
で設定することで適用できる - デフォルトの
MessageFormatter
はSimpleMessageFormatter
というクラス- このクラスは、単に
java.text.MessageFormat
を使ってdefaultMessageFormat
をフォーマットするだけになっている(messageKey
は使わない)
- このクラスは、単に
-
ResourceBundle
を使ってメッセージの解決を実装する例が公式ガイドに記載されている
ResourceBundleでメッセージを解決する例
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import am.ik.yavi.message.MessageFormatter;
public enum ResourceBundleMessageFormatter implements MessageFormatter {
SINGLETON;
@Override
public String format(
String messageKey, String defaultMessageFormat, Object[] args, Locale locale) {
ResourceBundle resourceBundle = ResourceBundle.getBundle("messages", locale);
String format;
try {
format = resourceBundle.getString(messageKey);
}
catch (MissingResourceException e) {
format = defaultMessageFormat;
}
try {
String target = resourceBundle.getString((String) args[0] /* field name */);
args[0] = target;
}
catch (MissingResourceException e) {
}
return new MessageFormat(format, locale).format(args);
}
}
違反情報と検査対象の値をひとまとめにして扱う
Hoge.java
package sandbox.yavi;
public record Hoge(String text) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(Hoge::text, "text", c -> c.notEmpty())
.build();
final Hoge hoge = new Hoge("");
final Validated<Hoge> hogeValidated = validator.applicative().validate(hoge);
System.out.println("valid = " + hogeValidated.isValid());
final Hoge result = hogeValidated
.mapError(ConstraintViolation::message)
.orElseThrow(messages -> new RuntimeException(messages.toString()));
System.out.println("result = " + result);
}
}
実行結果
valid = false
Exception in thread "main" java.lang.RuntimeException: ["text" must not be empty]
at sandbox.yavi.Main.lambda$main$1(Main.java:22)
at am.ik.yavi.fn.Validation.orElseThrow(Validation.java:131)
at sandbox.yavi.Main.main(Main.java:22)
説明
Main.java
final Validated<Hoge> hogeValidated = validator.applicative().validate(hoge);
-
Validator
のapplicative
メソッドを使用すると、ApplicativeValidator
というクラスが返される - このクラスの
validate
メソッドを使って検証を行うと、違反情報と検査対象の値をワンセットにしたValidated
オブジェクトが返されるようになる
Main.java
final Hoge result = hogeValidated
.mapError(ConstraintViolation::message)
.orElseThrow(messages -> new RuntimeException(messages.toString()));
-
mapError
やmap
など、オブジェクト内の違反情報または検査対象の値に対して関数を適用させるためのメソッドが用意されている - たぶん、関数型プログラミングっぽい実装をするのに役立つっぽい
-
value
やerrors
メソッドで検査対象の値や違反情報を取り出すことも可能-
value
で値が取れるのは、違反がない場合のみ - 違反がある状態で
value
を呼ぶと例外がスローされる
-
検査済みの値(Validated)を組み合わせる
Text.java
package sandbox.yavi;
public record Text(String value) {
}
Number.java
package sandbox.yavi;
public record Number(int value) {
}
Hoge.java
package sandbox.yavi;
public record Hoge(Text text, Number number) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ApplicativeValidator;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.Validated;
import am.ik.yavi.fn.Validation;
public class Main {
public static void main(String[] args) {
final ApplicativeValidator<Text> textValidator =
ValidatorBuilder.<Text>of()
.constraint(Text::value, "text", c -> c.notEmpty())
.build()
.applicative();
final ApplicativeValidator<Number> numberValidator =
ValidatorBuilder.<Number>of()
.constraint(Number::value, "number", c -> c.lessThan(10))
.build()
.applicative();
final Validated<Text> textValidated =
textValidator.validate(new Text(""));
final Validated<Number> numberValidated =
numberValidator.validate(new Number(100));
final Validation<ConstraintViolation, Hoge> hogeValidation =
textValidated.combine(numberValidated)
.apply(Hoge::new);
System.out.println("valid = " + hogeValidation.isValid());
hogeValidation
.mapError(ConstraintViolation::message)
.errors()
.forEach(System.out::println);
}
}
実行結果
valid = false
"text" must not be empty
"number" must be less than 10
説明
Main.java
final Validated<Text> textValidated =
textValidator.validate(new Text(""));
final Validated<Number> numberValidated =
numberValidator.validate(new Number(100));
final Validation<ConstraintViolation, Hoge> hogeValidation =
textValidated.combine(numberValidated)
.apply(Hoge::new);
- 2つ以上の
Validated
をcombine
メソッドで組み合わせることができる - 組み合わせた値(
Text
とNumber
) を元にして、新しい値(Hoge
)を構築できる-
apply
メソッドで元の値から新しい値に変換する方法を指定する
-
- 組み合わせた後の値(
Validation
)には、それまでに組み合わせた全ての検証結果の情報が含まれている- 最終的に違反がゼロだったら、組み合わせ後の新しい値(
Hoge
)を取得できる
- 最終的に違反がゼロだったら、組み合わせ後の新しい値(
- 3つ以上の値を一度に組み合わせたい場合は、
Validations
のcombine
メソッドが利用できる
3つ以上を組み合わせる場合
final Combining3<ConstraintViolation, Text, Number, Bool> combine
= Validations.combine(textValidated, numberValidated, boolValidated);
-
Validations
には、最大 16 個のValidation
を受け取るためのメソッドが用意されている
Validated
は Validation
のサブクラスなので、 Validation
を渡せる場所には Validated
も渡すことができる。
単一値のバリデーション
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.ValueValidator;
public class Main {
public static void main(String[] args) {
final StringValidator<String> textValidator
= StringValidatorBuilder.of("text", c -> c.notEmpty()).build();
printResult(textValidator, "");
printResult(textValidator, "hoge");
}
private static <T, X> void printResult(ValueValidator<T, X> validator, T target) {
final Validated<X> validated = validator.validate(target);
System.out.println(target + " > " + (validated.isValid() ? "valid" : "invalid"));
if (!validated.isValid()) {
validated.errors()
.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
}
実行結果
> invalid
"text" must not be empty
hoge > valid
説明
Main.java
final StringValidator<String> textValidator
= StringValidatorBuilder.of("text", c -> c.notEmpty()).build();
- 単一値用のバリデータを定義できる
- 検査対象の値の型ごとにバリデータとビルダーが用意されている
- 組み込みで以下のバリデータが用意されている
- プリミティブ型のラッパー用
BooleanValidator
DoubleValidator
FloatValidator
IntegerValidator
LongValidator
ShortValidator
- Date&Time API 用
InstantValidator
LocalDateTimeValidator
LocalDateValidator
LocalTimeValidator
OffsetDateTimeValidator
YearMonthValidator
YearValidator
ZonedDateTimeValidator
- その他基本的な型用
BigDecimalValidator
BigIntegerValidator
EnumValidator
ObjectValidator
StringValidator
- プリミティブ型のラッパー用
Main.java
final Validated<String> textValidated = textValidator.validate("");
- 検証結果が
Validated
として返される
検査 OK の場合に Value Object に変換する
Hoge.java
package sandbox.yavi;
public record Hoge(String text) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;
public class Main {
public static void main(String[] args) {
final StringValidator<Hoge> hogeValidator = StringValidatorBuilder
.of("text", c -> c.notEmpty())
.build()
.andThen(Hoge::new);
final Validated<Hoge> hogeValidated = hogeValidator.validate("test");
System.out.println("valid=" + hogeValidated.isValid());
final Hoge hoge = hogeValidated.value();
System.out.println("hoge=" + hoge);
}
}
実行結果
valid=true
hoge=Hoge[text=test]
説明
Main.java
final StringValidator<Hoge> hogeValidator = StringValidatorBuilder
.of("text", c -> c.notEmpty())
.build()
.andThen(Hoge::new);
- 検証 OK だった場合に Value Object へ変換する処理を
andThen
で登録できる
コレクションの検査
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;
import java.util.List;
public class Main {
public static void main(String[] args) {
final StringValidator<String> stringValidator = StringValidatorBuilder
.of("text", c -> c.notEmpty())
.build();
final Validated<List<String>> listValidated
= stringValidator.liftList().validate(List.of("a", "", "c"));
printViolations(listValidated.errors());
}
private static void printViolations(ConstraintViolations violations) {
System.out.println("valid=" + violations.isValid());
violations.stream()
.map(ConstraintViolation::message)
.forEach(System.out::println);
}
}
実行結果
valid=false
"text[1]" must not be empty
説明
Main.java
final Validated<List<String>> listValidated
= stringValidator.liftList().validate(List.of("a", "", "c"));
- 単一値のバリデータの
liftList
を使用することで、List
のバリデータへ変換できる -
List
以外の他のコレクション用にliftSet
,liftCollection(Supplier)
が用意されている-
liftOptional
というのもある
-
引数の検証
Hoge.java
package sandbox.yavi;
public record Hoge(String text, int number) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.Arguments1;
import am.ik.yavi.arguments.Arguments2;
import am.ik.yavi.arguments.Arguments2Validator;
import am.ik.yavi.builder.ArgumentsValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;
public class Main {
public static void main(String[] args) {
final Arguments2Validator<String, Integer, Hoge> validator =
ArgumentsValidatorBuilder.of(Hoge::new)
.builder(b -> b
._string(Arguments1::arg1, "text", c -> c.notEmpty())
._integer(Arguments2::arg2, "number", c -> c.lessThan(10))
).build();
final Validated<Hoge> hogeValidated = validator.validate("", 100);
printViolations(hogeValidated.errors());
}
private static void printViolations(ConstraintViolations violations) {
System.out.println("valid=" + violations.isValid());
violations.stream()
.map(ConstraintViolation::message)
.forEach(System.out::println);
}
}
実行結果
valid=false
"text" must not be empty
"number" must be less than 10
説明
- コンストラクタやファクトリメソッドのように、何らかのオブジェクトを生成するメソッドの引数を検証するための機能が用意されている
- この機能を利用すると、不正な状態のオブジェクトを作ることなく検査を実施できる
- 既に作成されたオブジェクトにだけ検査が行えるという方式だと、不正な状態のオブジェクトが生成できるようにしないといけない
- もしくは、検査のためだけの入れ物クラスが必要になる
Main.java
final Arguments2Validator<String, Integer, Hoge> validator =
ArgumentsValidatorBuilder.of(Hoge::new)
.builder(b -> b
._string(Arguments1::arg1, "text", c -> c.notEmpty())
._integer(Arguments2::arg2, "number", c -> c.lessThan(10))
).build();
final Validated<Hoge> hogeValidated = validator.validate("", 100);
-
ArgumentsValidatorBuilder
でビルダーを定義する-
of
メソッドに検査対象のメソッド or コンストラクタのメソッド参照を渡す -
builder
メソッドに渡す関数の中で、制約を定義する-
_string
のように、各引数の型に合わせたメソッドで制約を定義する - 第一引数には
Arguments1::arg1
のように引数の順番に合ったArgumentsN
型のメソッド参照を利用する
-
-
- 作成したバリデータの
validate
メソッドを使用すると、生成されるオブジェクトと違反情報を保持したValidated
オブジェクトが返される- 実際に目的のオブジェクト(
Hoge
)が生成されるのは違反がない場合に限られる
- 実際に目的のオブジェクト(
単一値のバリデータを用いて簡潔に定義する
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.Arguments2Validator;
import am.ik.yavi.arguments.ArgumentsValidators;
import am.ik.yavi.arguments.IntegerValidator;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.IntegerValidatorBuilder;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;
public class Main {
public static void main(String[] args) {
final StringValidator<String> textValidator = StringValidatorBuilder
.of("text", c -> c.notEmpty()).build();
final IntegerValidator<Integer> numberValidator = IntegerValidatorBuilder
.of("number", c -> c.lessThan(10)).build();
final Arguments2Validator<String, Integer, Hoge> hogeValidator
= ArgumentsValidators.split(textValidator, numberValidator)
.apply(Hoge::new);
final Validated<Hoge> hogeValidated = hogeValidator.validate("", 100);
printViolations(hogeValidated.errors());
}
private static void printViolations(ConstraintViolations violations) {
System.out.println("valid=" + violations.isValid());
violations.stream()
.map(ConstraintViolation::message)
.forEach(System.out::println);
}
}
実行結果
valid=false
"text" must not be empty
"number" must be less than 10
説明
Main.java
final Arguments2Validator<String, Integer, Hoge> hogeValidator
= ArgumentsValidators.split(textValidator, numberValidator)
.apply(Hoge::new);
- 単一値のバリデータを
ArgumentsValidators
のsplit
に渡すことで、引数検査のバリデータをより簡潔に定義できるようになる
Value Object が引数の場合
Text.java
package sandbox.yavi;
public record Text(String value) {
}
Number.java
package sandbox.yavi;
public record Number(int value) {
}
Hoge.java
package sandbox.yavi;
public record Hoge(Text text, Number number) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.Arguments2Validator;
import am.ik.yavi.arguments.ArgumentsValidators;
import am.ik.yavi.arguments.IntegerValidator;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.IntegerValidatorBuilder;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;
public class Main {
public static void main(String[] args) {
final StringValidator<Text> textValidator = StringValidatorBuilder
.of("text", c -> c.notEmpty())
.build()
.andThen(Text::new);
final IntegerValidator<Number> numberValidator = IntegerValidatorBuilder
.of("number", c -> c.lessThan(10))
.build()
.andThen(Number::new);
final Arguments2Validator<String, Integer, Hoge> hogeValidator
= ArgumentsValidators.split(textValidator, numberValidator)
.apply(Hoge::new);
final Validated<Hoge> hogeValidated = hogeValidator.validate("", 100);
printViolations(hogeValidated.errors());
}
private static void printViolations(ConstraintViolations violations) {
System.out.println("valid=" + violations.isValid());
violations.stream()
.map(ConstraintViolation::message)
.forEach(System.out::println);
}
}
実行結果
valid=false
"text" must not be empty
"number" must be less than 10
説明
Hoge.java
public record Hoge(Text text, Number number) {
- 検査対象の引数が Value Object の場合の実装方法
Main.java
final StringValidator<Text> textValidator = StringValidatorBuilder
.of("text", c -> c.notEmpty())
.build()
.andThen(Text::new);
final IntegerValidator<Number> numberValidator = IntegerValidatorBuilder
.of("number", c -> c.lessThan(10))
.build()
.andThen(Number::new);
final Arguments2Validator<String, Integer, Hoge> hogeValidator
= ArgumentsValidators.split(textValidator, numberValidator)
.apply(Hoge::new);
- 単一値のバリデータを
andThen
で Value Object へ変換できるようにする
通常の Validator を元にする
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.Arguments2Validator;
import am.ik.yavi.arguments.ArgumentsValidators;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final Validator<Text> textValidator = ValidatorBuilder.<Text>of()
.constraint(Text::value, "text", c -> c.notEmpty())
.build();
final Validator<Number> numberValidator = ValidatorBuilder.<Number>of()
.constraint(Number::value, "number", c -> c.lessThan(10))
.build();
final Arguments2Validator<Text, Number, Hoge> hogeValidator
= ArgumentsValidators.split(
textValidator.applicative(), numberValidator.applicative())
.apply(Hoge::new);
final Validated<Hoge> hogeValidated =
hogeValidator.validate(new Text(""), new Number(50));
printViolations(hogeValidated.errors());
}
private static void printViolations(ConstraintViolations violations) {
System.out.println("valid=" + violations.isValid());
violations.stream()
.map(ConstraintViolation::message)
.forEach(System.out::println);
}
}
実行結果
valid=false
"text" must not be empty
"number" must be less than 10
説明
Main.java
final Validator<Text> textValidator = ValidatorBuilder.<Text>of()
.constraint(Text::value, "text", c -> c.notEmpty())
.build();
final Validator<Number> numberValidator = ValidatorBuilder.<Number>of()
.constraint(Number::value, "number", c -> c.lessThan(10))
.build();
final Arguments2Validator<Text, Number, Hoge> hogeValidator
= ArgumentsValidators.split(
textValidator.applicative(), numberValidator.applicative())
.apply(Hoge::new);
- 単一値のバリデータではなく、
ValidatorBuilder
で作られた通常のバリデータをもとに引数検査のバリデータを生成する場合の方法 -
applicative
でApplicativeValidator
にするとsplit
メソッドへ渡せるようになる
生成対象のオブジェクトの元となるオブジェクトを検査対象にする
Text.java
package sandbox.yavi;
public record Text(String value) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.Arguments1Validator;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;
import java.util.Map;
public class Main {
public static void main(String[] args) {
final StringValidator<Text> textValidator = StringValidatorBuilder
.of("text", c -> c.notEmpty())
.build()
.andThen(Text::new);
final Arguments1Validator<Map<String, String>, Text> sourceTextValidator =
textValidator.compose(src -> src.get("text"));
final Map<String, String> source = Map.of("text", "");
final Validated<Text> textValidated = sourceTextValidator.validate(source);
printViolations(textValidated.errors());
}
private static void printViolations(ConstraintViolations violations) {
System.out.println("valid=" + violations.isValid());
violations.stream()
.map(ConstraintViolation::message)
.forEach(System.out::println);
}
}
実行結果
valid=false
"text" must not be empty
説明
Main.java
final Map<String, String> source = Map.of("text", "");
final Validated<Text> textValidated = sourceTextValidator.validate(source);
- 最終的な生成対象のオブジェクトは
Text
だが、その元となるオブジェクト(ソース)としてMap
を使用している -
validate
にはソースとなるMap
を渡して検査を行い、 OK であればText
で結果を取得できる形でValidated
を受け取っている - 例えば Web アプリなどでは
HttpServletRequest
からパラメータを取得してドメインオブジェクトを構築する、といった流れが考えられる - この機能は、そういった場面で活用できる
Main.java
final Arguments1Validator<Map<String, String>, Text> sourceTextValidator =
textValidator.compose(src -> src.get("text"));
- ソースオブジェクトから検査対象の値を取り出す関数を、元となる単一値バリデータの
compose
メソッドに渡す
バリデータを組み合わせる
Text.java
package sandbox.yavi;
public record Text(String value) {
}
Number.java
package sandbox.yavi;
public record Number(int value) {
}
Hoge.java
package sandbox.yavi;
public record Hoge(Text text, Number number) {
}
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.Arguments1Validator;
import am.ik.yavi.arguments.ArgumentsValidators;
import am.ik.yavi.builder.IntegerValidatorBuilder;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;
import java.util.Map;
public class Main {
public static void main(String[] args) {
final Arguments1Validator<Map<String, String>, Text> sourceTextValidator =
StringValidatorBuilder
.of("text", c -> c.notEmpty())
.build()
.andThen(Text::new)
.compose(src -> src.get("text"));
final Arguments1Validator<Map<String, String>, Number> sourceNumberValidator =
IntegerValidatorBuilder
.of("number", c -> c.lessThan(10))
.build()
.andThen(Number::new)
.compose(src -> Integer.valueOf(src.get("number")));
final Map<String, String> source = Map.of(
"text", "",
"number", "50"
);
final Arguments1Validator<Map<String, String>, Hoge> sourceHogeValidator
= ArgumentsValidators
.combine(sourceTextValidator, sourceNumberValidator)
.apply(Hoge::new);
final Validated<Hoge> hogeValidated = sourceHogeValidator.validate(source);
printViolations(hogeValidated.errors());
}
private static void printViolations(ConstraintViolations violations) {
System.out.println("valid=" + violations.isValid());
violations.stream()
.map(ConstraintViolation::message)
.forEach(System.out::println);
}
}
実行結果
valid=false
"text" must not be empty
"number" must be less than 10
説明
Main.java
final Arguments1Validator<Map<String, String>, Hoge> sourceHogeValidator
= ArgumentsValidators
.combine(sourceTextValidator, sourceNumberValidator)
.apply(Hoge::new);
- 複数のソースを検査するバリデータは、
ArgumentsValidators
のcombine
で1つにまとめることができる
Validator と ValueValidator
途中から Validated
とか単一値のバリデータが出てきて各クラスの関係性が分かりにくくなったので整理する。
- 基本となる
Validator
は、単に検査対象の値を受け取って違反情報(ConstraintViolations
)を返す - 一方で、
ValueValidator
は検査対象の値と違反情報をワンセットにしたValidated
を返す -
applicative
で作成するApplicativeValidator
や単一値バリデータであるStringValidator
などはValueValidator
のサブタイプに該当する
アノテーションプロセッサー
build.gradle
dependencies {
implementation "am.ik.yavi:yavi:0.14.0"
annotationProcessor "am.ik.yavi:yavi:0.14.0" // ★追加
}
IntelliJ の設定
- [File] > [Settings] で設定画面を開く
- [Build, Execution, Deployment] > [Compiler] > [Annotation Processors] を選択
- [Enable annotation processing] のチェックを オン にする
Hoge.java
package sandbox.yavi;
import am.ik.yavi.meta.ConstraintTarget;
public class Hoge {
private final String text;
private final int number;
public Hoge(String text, int number) {
this.text = text;
this.number = number;
}
@ConstraintTarget
public String getText() {
return text;
}
@ConstraintTarget
public int getNumber() {
return number;
}
}
Main.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.ConstraintViolation;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(_HogeMeta.TEXT, c -> c.notEmpty())
.constraint(_HogeMeta.NUMBER, c -> c.lessThan(10))
.build();
final ConstraintViolations violations = validator.validate(new Hoge("", 50));
printViolations(violations);
}
private static void printViolations(ConstraintViolations violations) {
System.out.println("valid=" + violations.isValid());
violations.stream()
.map(ConstraintViolation::message)
.forEach(System.out::println);
}
}
実行結果
valid=false
"text" must not be empty
"number" must be less than 10
説明
build.gradle
dependencies {
implementation "am.ik.yavi:yavi:0.14.0"
annotationProcessor "am.ik.yavi:yavi:0.14.0" // ★追加
}
- アノテーションプロセッサーを有効にするため、
annotationProcessor
の指定を追加する - IDE を使っている場合は、 IDE でもアノテーションプロセッサーが有功になるように設定を変更する
Hoge.java
@ConstraintTarget
public String getText() {
return text;
}
@ConstraintTarget
public int getNumber() {
return number;
}
- 検査対象となるプロパティの Getter に
@ConstraintTarget
を設定する
_HogeMeta.java
package sandbox.yavi;
// Generated at 2024-06-23T09:53:55.930424700+09:00
public class _HogeMeta {
public static final am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge> TEXT
= new am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge>() {
@Override
public String name() {
return "text";
}
@Override
public java.util.function.Function<sandbox.yavi.Hoge, java.lang.String> toValue() {
return sandbox.yavi.Hoge::getText;
}
};
public static final am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge> NUMBER
= new am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge>() {
@Override
public String name() {
return "number";
}
@Override
public java.util.function.Function<sandbox.yavi.Hoge, java.lang.Integer> toValue() {
return sandbox.yavi.Hoge::getNumber;
}
};
}
- プロジェクトをビルドしてアノテーションプロセッサーを実行すると、
_HogeMeta
というクラスが自動生成される- クラス名は、対象クラス名の先頭にアンダーバー (
_
) が付き、末尾にMeta
が追加されたものになる
- クラス名は、対象クラス名の先頭にアンダーバー (
- Gradle の場合は
build/generated/sources/annotationProcessor/java
の下に出力される
Main.java
final Validator<Hoge> validator = ValidatorBuilder.<Hoge>of()
.constraint(_HogeMeta.TEXT, c -> c.notEmpty())
.constraint(_HogeMeta.NUMBER, c -> c.lessThan(10))
.build();
- 生成されたメタクラスを利用すると、制約を定義するときに名前の指定を省略できるようになる
コンストラクタ引数で指定する
Hoge.java
package sandbox.yavi;
import am.ik.yavi.meta.ConstraintTarget;
public class Hoge {
private final String text;
private final int number;
public Hoge(@ConstraintTarget String text, @ConstraintTarget int number) {
this.text = text;
this.number = number;
}
public String getText() {
return text;
}
public int getNumber() {
return number;
}
}
- Getter の名前が Java Beans の仕様に沿っている場合は、コンストラクタ引数に
@ConstraintTarget
を設定することでもメタクラスを生成できる
生成されたメタクラス
package sandbox.yavi;
// Generated at 2024-06-23T10:05:26.182380100+09:00
public class _HogeMeta {
public static final am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge> TEXT
= new am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge>() {
@Override
public String name() {
return "text";
}
@Override
public java.util.function.Function<sandbox.yavi.Hoge, java.lang.String> toValue() {
return sandbox.yavi.Hoge::getText;
}
};
public static final am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge> NUMBER
= new am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge>() {
@Override
public String name() {
return "number";
}
@Override
public java.util.function.Function<sandbox.yavi.Hoge, java.lang.Integer> toValue() {
return sandbox.yavi.Hoge::getNumber;
}
};
}
Getter の名前が項目名と等しい場合
Hoge.java
package sandbox.yavi;
import am.ik.yavi.meta.ConstraintTarget;
public class Hoge {
private final String text;
private final int number;
public Hoge(
@ConstraintTarget(getter = false) String text,
@ConstraintTarget(getter = false) int number) {
this.text = text;
this.number = number;
}
public String text() {
return text;
}
public int number() {
return number;
}
}
- Getter の名前を項目名と同じにしている場合は、
@ConstraintTarget
のgetter
にfalse
を設定する
生成されたメタクラス
package sandbox.yavi;
// Generated at 2024-06-23T10:27:21.116552100+09:00
public class _HogeMeta {
public static final am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge> TEXT
= new am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge>() {
@Override
public String name() {
return "text";
}
@Override
public java.util.function.Function<sandbox.yavi.Hoge, java.lang.String> toValue() {
return sandbox.yavi.Hoge::text;
}
};
public static final am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge> NUMBER
= new am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge>() {
@Override
public String name() {
return "number";
}
@Override
public java.util.function.Function<sandbox.yavi.Hoge, java.lang.Integer> toValue() {
return sandbox.yavi.Hoge::number;
}
};
}
フィールドアクセスにする
Hoge.java
package sandbox.yavi;
import am.ik.yavi.meta.ConstraintTarget;
public class Hoge {
final String text;
final int number;
public Hoge(
@ConstraintTarget(field = true) String text,
@ConstraintTarget(field = true) int number) {
this.text = text;
this.number = number;
}
}
- Getter を用意したくない場合は、メタクラスからのアクセスをフィールド直接のアクセスにできる
- この場合、
@ConstraintTarget
のfield
にtrue
を設定する - 自動生成されるメタクラスは対象クラスと同じパッケージに出力されるので、フィールドはパッケージプライベート以上の可視性にしておく必要がある
生成されたメタクラス
package sandbox.yavi;
// Generated at 2024-06-23T10:27:52.651064400+09:00
public class _HogeMeta {
public static final am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge> TEXT
= new am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge>() {
@Override
public String name() {
return "text";
}
@Override
public java.util.function.Function<sandbox.yavi.Hoge, java.lang.String> toValue() {
return x -> x.text;
}
};
public static final am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge> NUMBER
= new am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge>() {
@Override
public String name() {
return "number";
}
@Override
public java.util.function.Function<sandbox.yavi.Hoge, java.lang.Integer> toValue() {
return x -> x.number;
}
};
}
Record クラスの場合
Hoge.java
package sandbox.yavi;
import am.ik.yavi.meta.ConstraintTarget;
public record Hoge(@ConstraintTarget String text, @ConstraintTarget int number) {
}
- Record クラスの場合は、レコードコンポーネントに
@ConstraintTarget
を設定する - このとき
getter = false
は不要
生成されたメタクラス
package sandbox.yavi;
// Generated at 2024-06-23T10:30:29.492874400+09:00
public class _HogeMeta {
public static final am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge> TEXT
= new am.ik.yavi.meta.StringConstraintMeta<sandbox.yavi.Hoge>() {
@Override
public String name() {
return "text";
}
@Override
public java.util.function.Function<sandbox.yavi.Hoge, java.lang.String> toValue() {
return sandbox.yavi.Hoge::text;
}
};
public static final am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge> NUMBER
= new am.ik.yavi.meta.IntegerConstraintMeta<sandbox.yavi.Hoge>() {
@Override
public String name() {
return "number";
}
@Override
public java.util.function.Function<sandbox.yavi.Hoge, java.lang.Integer> toValue() {
return sandbox.yavi.Hoge::number;
}
};
}
ビルトインの制約
デフォルトメッセージが何なのかや、埋め込まれるパラメータなどの細かい情報は本家ガイドを参照。
ここでは気になったポイントに絞ってメモする。
String
文字数のカウント
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.ValueValidator;
public class Main {
public static void main(String[] args) {
final StringValidator<String> validator = StringValidatorBuilder
.of("text", c -> c.lessThanOrEqual(3)).build();
printResult(validator, "𠮷野家");
printResult(validator, "𠮷野家!");
printResult(validator, "abc");
printResult(validator, "abcd");
}
private static <T, X> void printResult(ValueValidator<T, X> validator, T target) {
final Validated<X> validated = validator.validate(target);
System.out.println(target + " > " + (validated.isValid() ? "valid" : "invalid"));
if (!validated.isValid()) {
validated.errors()
.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
}
実行結果
𠮷野家 > valid
𠮷野家! > invalid
The size of "text" must be less than or equal to 3. The given size is 4
abc > valid
abcd > invalid
The size of "text" must be less than or equal to 3. The given size is 4
- YAVI は、文字数のカウントをするときに以下のケースを考慮する
- サロゲートペア
- 結合文字
- 異体字セレクタ
- 絵文字
- サロゲートペアと結合文字についてはデフォルトで有効になっており、見た目と文字数が一致する形で検査される
- 異体字セレクタと絵文字については、パフォーマンスを優先してデフォルトは無効となっている
- 絵文字については「できるだけ見た目と文字数が一致するようにしているが保証はできない」というスタンス
- 詳しくは公式ドキュメントを参照
- YAVI は、文字列が email アドレス形式であることを検証する制約をサポートしている
- email アドレスのフォーマットといえばバリデーションが難しいことで有名?
- YAVI の email アドレスのチェックが何に準拠しているのか、ドキュメントやソースコードには特に明記は無かった
- チェックで使用している正規表現は以下で定義されている
- 正規表現を抜き出すと、以下のようになっている
^[^\x00-\x1F()<>@,;:\\".\[\]\s]+(\.[^\x00-\x1F()<>@,;:\\".\[\]\s]+)*@([^\x00-\x1F()<>@,;:\\".\[\]\s]+(\.[^\x00-\x1F()<>@,;:\\".\[\]\s]+)*|^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$)$
バイト数での判定
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;
import am.ik.yavi.core.ValueValidator;
import java.nio.charset.Charset;
public class Main {
public static void main(String[] args) {
final StringValidator<String> validator = StringValidatorBuilder
.of("text",
c -> c.asByteArray(Charset.forName("MS932")).lessThan(4))
.build();
printResult(validator, "abc");
printResult(validator, "あイ");
printResult(validator, "あい");
}
private static <T, X> void printResult(ValueValidator<T, X> validator, T target) {
final Validated<X> validated = validator.validate(target);
System.out.println(target + " > " + (validated.isValid() ? "valid" : "invalid"));
if (!validated.isValid()) {
validated.errors()
.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
}
実行結果
abc > valid
あイ > valid
あい > invalid
The byte size of "text" must be less than 4. The given size is 4
-
asByteArray
で文字列を byte 配列にした場合のサイズを検証できるようになる - 引数の文字コードの指定を省略した場合は、デフォルトで
UTF-8
でエンコードされる
password
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;
public class Main {
public static void main(String[] args) {
final StringValidator<String> validator = StringValidatorBuilder
.of("password", c -> c.password(policy ->
policy.uppercase().lowercase().numbers().build()
))
.build();
printResult(validator, "Aa11");
printResult(validator, "Aa1#");
printResult(validator, "aa11");
}
private static void printResult(StringValidator<String> validator, String password) {
final Validated<String> validated = validator.validate(password);
System.out.println(password + " > " + (validated.isValid() ? "valid" : "invalid"));
if (!validated.isValid()) {
validated.errors()
.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
}
実行結果
Aa11 > valid
Aa1# > valid
aa11 > invalid
"password" must meet Uppercase policy
説明
Main.java
final StringValidator<String> validator = StringValidatorBuilder
.of("password", c -> c.password(policy ->
policy.uppercase().lowercase().numbers().build()
))
.build();
- パスワードの文字種チェックを定義できる
-
password
の引数にパスワードポリシーを定義する関数を渡す-
uppercase
やlowercase
など、含めなければならない文字種を定義するメソッドが用意されている -
upercase()
,lowercase()
は、最低1文字、その文字種が含まれている必要があることを定義している - したがって、上記実装例は「大文字・小文字・数字が、それぞれ最低1文字含まれている必要がある」というポリシーを定義していることになる
-
指定していない文字種は任意扱い?
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;
public class Main {
public static void main(String[] args) {
final StringValidator<String> validator = StringValidatorBuilder
.of("password", c -> c.password(policy ->
policy.uppercase().build()
))
.build();
printResult(validator, "AAAA");
printResult(validator, "Aa1#");
printResult(validator, "aa1#");
}
private static void printResult(StringValidator<String> validator, String password) {
final Validated<String> validated = validator.validate(password);
System.out.println(password + " > " + (validated.isValid() ? "valid" : "invalid"));
if (!validated.isValid()) {
validated.errors()
.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
}
実行結果
AAAA > valid
Aa1# > valid
aa1# > invalid
"password" must meet Uppercase policy
-
uppercase
だけを定義したポリシーに対してAa1#
が valid と判定されている - ポリシーで指定された文字種以外は任意扱いになるっぽい
- 「指定文字種以外は含めてはいけない」を実現しようと思うと、
pattern
で否定の正規表現を指定するか、独自の制約を定義する必要がありそう?
最低文字数を指定する
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.core.Validated;
public class Main {
public static void main(String[] args) {
final StringValidator<String> validator = StringValidatorBuilder
.of("password", c -> c.password(policy ->
policy.uppercase(2).lowercase(3).build()
))
.build();
printResult(validator, "ABcde");
printResult(validator, "Abcd");
printResult(validator, "Abc");
}
private static void printResult(StringValidator<String> validator, String password) {
final Validated<String> validated = validator.validate(password);
System.out.println(password + " > " + (validated.isValid() ? "valid" : "invalid"));
if (!validated.isValid()) {
validated.errors()
.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
}
実行結果
ABcde > valid
Abcd > invalid
"password" must meet Uppercase policy
Abc > invalid
"password" must meet Uppercase policy
"password" must meet Lowercase policy
-
uppercase
などの文字種を指定するメソッドの引数で最低文字数(1以上)を指定できる
任意のポリシーの組み合わせを選択できるようにする
Main.java
package sandbox.yavi;
import am.ik.yavi.arguments.StringValidator;
import am.ik.yavi.builder.StringValidatorBuilder;
import am.ik.yavi.constraint.password.PasswordPolicy;
import am.ik.yavi.core.Validated;
public class Main {
public static void main(String[] args) {
final StringValidator<String> validator = StringValidatorBuilder
.of("password", c -> c.password(policy ->
policy.optional(
2,
PasswordPolicy.LOWERCASE,
PasswordPolicy.UPPERCASE,
PasswordPolicy.NUMBERS)
.build()
))
.build();
printResult(validator, "aA");
printResult(validator, "a1");
printResult(validator, "A1");
printResult(validator, "a#");
}
private static void printResult(StringValidator<String> validator, String password) {
final Validated<String> validated = validator.validate(password);
System.out.println(password + " > " + (validated.isValid() ? "valid" : "invalid"));
if (!validated.isValid()) {
validated.errors()
.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
}
実行結果
aA > valid
a1 > valid
A1 > valid
a# > invalid
"password" must meet at least 2 policies from [Lowercase, Uppercase, Numbers]
説明
final StringValidator<String> validator = StringValidatorBuilder
.of("password", c -> c.password(policy ->
policy.optional(
2,
PasswordPolicy.LOWERCASE,
PasswordPolicy.UPPERCASE,
PasswordPolicy.NUMBERS)
.build()
))
.build();
-
optional
を使用すると、いくつかのポリシーの中から任意の n 個のポリシーを選択する、という制約を定義できる - この場合、
LOWERCASE
(小文字),UPPERCASE
(大文字),NMBERS
(数字) の3つのポリシーのうち任意の2つを選択できる、という定義になる
Getter で公開していない項目を検査する
Hoge.java
package sandbox.yavi;
import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.Validator;
public class Hoge {
private final String text;
public static final Validator<Hoge> VALIDATOR = ValidatorBuilder.<Hoge>of()
._string(h -> h.text, "text", c -> c.notEmpty())
.build();
public Hoge(String text) {
this.text = text;
}
@Override
public String toString() {
return "Hoge{" + "text='" + text + '\'' + '}';
}
}
Main.java
package sandbox.yavi;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
public class Main {
public static void main(String[] args) {
printResult(Hoge.VALIDATOR, new Hoge(""));
printResult(Hoge.VALIDATOR, new Hoge("a"));
}
private static <T> void printResult(Validator<T> validator, T target) {
final ConstraintViolations violations = validator.validate(target);
System.out.println(
target + " > " + (violations.isValid() ? "valid" : "invalid"));
if (!violations.isValid()) {
violations.stream()
.map(e -> " " + e.message())
.forEach(System.out::println);
}
}
}
実行結果
Hoge{text=''} > invalid
"text" must not be empty
Hoge{text='a'} > valid
- クラス内にバリデータを定義することで、フィールドを非公開のまま検査できるように実装できる
Spring で使用する
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'sandbox.yavi'
version = '1.0.0'
compileJava.options.encoding = "UTF-8"
java {
sourceCompatibility = '21'
targetCompatibility = '21'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation "am.ik.yavi:yavi:0.14.0"
}
YaviApplication
package sandbox.yavi;
import am.ik.yavi.factory.ValidatorFactory;
import am.ik.yavi.message.MessageSourceMessageFormatter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class YaviApplication {
public static void main(String[] args) {
try (final ConfigurableApplicationContext context
= SpringApplication.run(YaviApplication.class, args)) {
final HogeService service = context.getBean(HogeService.class);
service.execute(new Hoge(""));
service.execute(new Hoge("a"));
}
}
@Bean
public ValidatorFactory yaviValidatorFactory(MessageSource messageSource) {
final MessageSourceMessageFormatter formatter
= new MessageSourceMessageFormatter(messageSource::getMessage);
return new ValidatorFactory(".", formatter);
}
}
Hoge.java
package sandbox.yavi;
public record Hoge(String text) {
}
HogeService.java
package sandbox.yavi;
import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validator;
import am.ik.yavi.factory.ValidatorFactory;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class HogeService {
@Autowired
private ValidatorFactory validatorFactory;
private Validator<Hoge> hogeValidator;
@PostConstruct
public void init() {
hogeValidator = validatorFactory.validator(
b -> b.constraint(Hoge::text, "text", c -> c.notEmpty()));
}
public void execute(Hoge hoge) {
final ConstraintViolations violations = hogeValidator.validate(hoge);
System.out.println(hoge + " > " + (violations.isValid() ? "valid" : "invalid"));
violations.stream()
.map(v -> " " + v.message())
.forEach(System.out::println);
}
}
messages.properties
container.notEmpty="{0}" must not be empty!!!!
実行結果
Hoge[text=] > invalid
"text" must not be empty!!!!
Hoge[text=a] > valid
説明
YaviApplication.java
@Bean
public ValidatorFactory yaviValidatorFactory(MessageSource messageSource) {
final MessageSourceMessageFormatter formatter
= new MessageSourceMessageFormatter(messageSource::getMessage);
- Spring が管理する
MessageSource
と YAVI のMessageFormatter
を繋げるために、MessageSourceMessageFormatter
というクラスが用意されている-
MessageSourceMessageFormatter
のコンストラクタに Spring のMessageSource
のgetMessage
をメソッド参照で渡す
-
YaviApplicaiton.java
return new ValidatorFactory(".", formatter);
- 共通の設定をした
Validator
の生成を補助するクラスとしてValidatorFactory
というクラスが用意されている - コンストラクタの第一引数には、メッセージキーの区切り文字を渡す
- 第二引数には
MessageFormatter
を渡す
HogeService
@Autowired
private ValidatorFactory validatorFactory;
private Validator<Hoge> hogeValidator;
@PostConstruct
public void init() {
hogeValidator = validatorFactory.validator(
b -> b.constraint(Hoge::text, "text", c -> c.notEmpty()));
}
-
ValidatorFactory
のvalidator
メソッドで新しいValidator
を生成できる