LoginSignup
1
1

YAVI使い方メモ

Last updated at Posted at 2024-06-23

YAVIとは?

  • Java のバリデーションライブラリ
  • ラムダを使用し、型安全な API を提供する
  • Yet Another Validation for Java の略で、読みはヤバイ
  • 日本人の @making さんが作っている
  • Spring WebFlux.fn で使えるバリデーションが欲しいというのが動機で、 Bean Validation を置き換えるのが目的ではない

環境

>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"
}
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);
        }
    }
  • Validatorvalidate メソッドに検査対象のオブジェクトを渡すことで、違反情報が格納された ConstraintViolations が返される
  • isValid メソッドで全体のバリデーション結果が分かる
  • ConstraintViolationsConstraintViolationList となっており、個々の違反結果の詳細は 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 およびそのサブタイプ)が渡される
      • notNullgreaterThan など、制約を定義するためのメソッドが多数用意されていて、これらをメソッドチェーンで繋げることによって制約を定義する
  • Validatorvalidate メソッドに検査対象のオブジェクトを渡すことで、検査を実行できる

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);
        }
    }
  • Validatorvalidate で検索を実行すると、違反情報が ConstraintViolations という型で返される
  • ConstraintViolationsConstraintViolationList となっていて、 ConstraintViolation に個々の違反内容が格納されている
    • 違反が1つ以上あるかどうかは isValid メソッドで確認できる
    • 違反がゼロの場合は、空になっている
  • 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
}
  • ConstraintViolationsdetails または ConstraintViolationdetail メソッドを使用すると、違反情報を 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 を違反としたい場合は、 notNullnotEmpty, 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
  • ValidatorfailFast メソッドを使うことで、既存の 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
  • ListSet、配列の制約を定義する場合は、 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) {
}
  • こんな感じで、 StringInteger などのラッパークラスのコレクションを持つ項目に対して制約を定義する場合を考える
  • 無理やり書くと、以下のようになる
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 を返す
  • 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 で設定することで適用できる
  • デフォルトの MessageFormatterSimpleMessageFormatter というクラス
    • このクラスは、単に 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);
  • Validatorapplicative メソッドを使用すると、 ApplicativeValidator というクラスが返される
  • このクラスの validate メソッドを使って検証を行うと、違反情報と検査対象の値をワンセットにした Validated オブジェクトが返されるようになる
Main.java
        final Hoge result = hogeValidated
                .mapError(ConstraintViolation::message)
                .orElseThrow(messages -> new RuntimeException(messages.toString()));
  • mapErrormap など、オブジェクト内の違反情報または検査対象の値に対して関数を適用させるためのメソッドが用意されている
  • たぶん、関数型プログラミングっぽい実装をするのに役立つっぽい
  • valueerrors メソッドで検査対象の値や違反情報を取り出すことも可能
    • 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つ以上の Validatedcombine メソッドで組み合わせることができる
  • 組み合わせた値(TextNumber) を元にして、新しい値(Hoge)を構築できる
    • apply メソッドで元の値から新しい値に変換する方法を指定する
  • 組み合わせた後の値(Validation)には、それまでに組み合わせた全ての検証結果の情報が含まれている
    • 最終的に違反がゼロだったら、組み合わせ後の新しい値(Hoge)を取得できる
  • 3つ以上の値を一度に組み合わせたい場合は、 Validationscombine メソッドが利用できる
3つ以上を組み合わせる場合
        final Combining3<ConstraintViolation, Text, Number, Bool> combine
                = Validations.combine(textValidated, numberValidated, boolValidated);
  • Validations には、最大 16 個の Validation を受け取るためのメソッドが用意されている

ValidatedValidation のサブクラスなので、 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);
  • 単一値のバリデータを ArgumentsValidatorssplit に渡すことで、引数検査のバリデータをより簡潔に定義できるようになる

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 で作られた通常のバリデータをもとに引数検査のバリデータを生成する場合の方法
  • applicativeApplicativeValidator にすると 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);
  • 複数のソースを検査するバリデータは、 ArgumentsValidatorscombine で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] のチェックを オン にする

image.png

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 の名前を項目名と同じにしている場合は、 @ConstraintTargetgetterfalse を設定する
生成されたメタクラス
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 を用意したくない場合は、メタクラスからのアクセスをフィールド直接のアクセスにできる
  • この場合、 @ConstraintTargetfieldtrue を設定する
  • 自動生成されるメタクラスは対象クラスと同じパッケージに出力されるので、フィールドはパッケージプライベート以上の可視性にしておく必要がある
生成されたメタクラス
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 は、文字数のカウントをするときに以下のケースを考慮する
    • サロゲートペア
    • 結合文字
    • 異体字セレクタ
    • 絵文字
  • サロゲートペアと結合文字についてはデフォルトで有効になっており、見た目と文字数が一致する形で検査される
  • 異体字セレクタと絵文字については、パフォーマンスを優先してデフォルトは無効となっている
    • 絵文字については「できるだけ見た目と文字数が一致するようにしているが保証はできない」というスタンス
    • 詳しくは公式ドキュメントを参照

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 の引数にパスワードポリシーを定義する関数を渡す
    • uppercaselowercase など、含めなければならない文字種を定義するメソッドが用意されている
    • 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 の MessageSourcegetMessage をメソッド参照で渡す
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()));
    }
  • ValidatorFactoryvalidator メソッドで新しい Validator を生成できる

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1