YAVI (Yet Another ValIdation)

バリデーションライブラリJavaラムダ式型安全関数型プログラミングゼロ依存

GitHub概要

making/yavi

Yet Another Validation for Java (A lambda based type safe validation framework)

ホームページ:https://yavi.ik.am
スター830
ウォッチ22
フォーク63
作成日:2018年8月21日
言語:Java
ライセンス:Apache License 2.0

トピックス

javakotlinvalidationvalidation-libraryvalidator

スター履歴

making/yavi Star History
データ取得日時: 2025/10/22 08:07

ライブラリ

YAVI (Yet Another ValIdation)

概要

YAVI(ヤバイ)は、日本人開発者により作成されたラムダ式ベースの型安全なJavaバリデーションライブラリです。リフレクションやランタイムアノテーションを使用せず、関数型プログラミングの概念を取り入れた軽量で高性能なバリデーションフレームワークです。Java Beansに限定されず、Records、Protocol Buffers、Immutablesなど、あらゆるオブジェクトに対してバリデーションを適用できます。

詳細

YAVIは「Yet Another ValIdation」の略で、「ヤバイ」という日本語のスラングと同じ発音になるように命名されています。2025年現在、バージョン0.16.0がリリースされており、Bean Validationに代わる軽量で柔軟なバリデーションソリューションとして注目を集めています。完全にゼロ依存で動作し、ラムダ式による直感的なAPIを提供します。

主な特徴

  • ゼロ依存: 外部ライブラリを一切使用しない軽量設計
  • リフレクション不使用: 実行時のパフォーマンスペナルティなし
  • 型安全: コンパイル時に型チェックが行われる
  • 関数型アプローチ: Applicative Functorパターンの実装
  • 柔軟な対象: Java Beans、Records、Protocol Buffers等に対応
  • メッセージ補間: 柔軟なエラーメッセージのカスタマイズ

メリット・デメリット

メリット

  • Bean Validationよりも軽量で高速
  • ラムダ式による簡潔で読みやすいコード
  • 完全な型安全性によるコンパイル時エラー検出
  • リフレクション不使用による予測可能な動作
  • 小さなバリデータの組み合わせによる柔軟な構成
  • Spring BootやJakarta EEに依存しない独立性

デメリット

  • Bean Validationと比較して知名度が低い
  • アノテーション方式に慣れた開発者には学習コストがある
  • IDEのサポートがBean Validationほど充実していない
  • エンタープライズ標準ではないため採用に慎重な判断が必要
  • ドキュメントが英語と日本語のみ

参考ページ

書き方の例

インストール(Maven)

<dependency>
    <groupId>am.ik.yavi</groupId>
    <artifactId>yavi</artifactId>
    <version>0.16.0</version>
</dependency>

インストール(Gradle)

implementation 'am.ik.yavi:yavi:0.16.0'

基本的なバリデーション

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

// シンプルなRecordの定義
public record User(String name, String email, Integer age) {}

// バリデータの定義
public class UserValidator {
    public static final Validator<User> validator = ValidatorBuilder.<User>of()
        .constraint(User::name, "name", c -> c.notBlank()
            .lessThanOrEqual(50))
        .constraint(User::email, "email", c -> c.notBlank()
            .email())
        .constraint(User::age, "age", c -> c.notNull()
            .greaterThanOrEqual(0)
            .lessThanOrEqual(150))
        .build();
}

// 使用例
public class Main {
    public static void main(String[] args) {
        User user = new User("田中太郎", "[email protected]", 30);
        
        ConstraintViolations violations = UserValidator.validator.validate(user);
        
        if (violations.isValid()) {
            System.out.println("バリデーション成功");
        } else {
            violations.forEach(violation -> 
                System.out.println(violation.message()));
        }
    }
}

複雑なバリデーションルール

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.Validator;
import java.time.LocalDate;
import java.util.List;

public record Product(
    String id,
    String name,
    Double price,
    List<String> categories,
    LocalDate releaseDate,
    Integer stock
) {}

public class ProductValidator {
    // カスタム制約を含む複雑なバリデータ
    public static final Validator<Product> validator = ValidatorBuilder.<Product>of()
        .constraint(Product::id, "id", c -> c.notNull()
            .pattern("^PRD-\\d{6}$")
            .message("商品IDはPRD-で始まる6桁の数字である必要があります"))
        
        .constraint(Product::name, "name", c -> c.notBlank()
            .greaterThanOrEqual(2)
            .lessThanOrEqual(100))
        
        .constraint(Product::price, "price", c -> c.notNull()
            .greaterThan(0.0)
            .lessThan(1_000_000.0))
        
        .constraint(Product::categories, "categories", c -> c.notEmpty()
            .lessThanOrEqual(5))
        
        .constraintOnEach(Product::categories, "categories", c -> c.notBlank())
        
        .constraint(Product::releaseDate, "releaseDate", c -> c.notNull()
            .beforeOrEqual(() -> LocalDate.now().plusYears(1))
            .afterOrEqual(() -> LocalDate.now().minusYears(10)))
        
        .constraint(Product::stock, "stock", c -> c.greaterThanOrEqual(0))
        
        // 条件付きバリデーション
        .constraintOnCondition((product, group) -> product.stock() != null && product.stock() > 0,
            b -> b.constraint(Product::price, "price", c -> c.greaterThan(10.0)
                .message("在庫がある商品の価格は10円以上である必要があります")))
        
        .build();
}

// 使用例
public class ProductValidationExample {
    public static void main(String[] args) {
        Product product = new Product(
            "PRD-123456",
            "高性能ノートPC",
            150000.0,
            List.of("電子機器", "コンピュータ", "ノートPC"),
            LocalDate.now().plusMonths(2),
            50
        );
        
        validator.validate(product)
            .ifValid(p -> System.out.println("商品 " + p.name() + " は有効です"))
            .ifInvalid(violations -> {
                System.out.println("バリデーションエラー:");
                violations.forEach(v -> 
                    System.out.println("  - " + v.name() + ": " + v.message()));
            });
    }
}

ネストされたオブジェクトのバリデーション

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

// 住所
public record Address(
    String postalCode,
    String prefecture,
    String city,
    String street
) {}

// 顧客
public record Customer(
    String id,
    String name,
    String email,
    Address address,
    List<String> phoneNumbers
) {}

public class NestedValidation {
    // 住所バリデータ
    public static final Validator<Address> addressValidator = ValidatorBuilder.<Address>of()
        .constraint(Address::postalCode, "postalCode", c -> c.notBlank()
            .pattern("^\\d{3}-\\d{4}$")
            .message("郵便番号はXXX-XXXX形式で入力してください"))
        .constraint(Address::prefecture, "prefecture", c -> c.notBlank())
        .constraint(Address::city, "city", c -> c.notBlank())
        .constraint(Address::street, "street", c -> c.notBlank()
            .lessThanOrEqual(200))
        .build();
    
    // 顧客バリデータ(ネストされたバリデーションを含む)
    public static final Validator<Customer> customerValidator = ValidatorBuilder.<Customer>of()
        .constraint(Customer::id, "id", c -> c.notBlank())
        .constraint(Customer::name, "name", c -> c.notBlank()
            .greaterThanOrEqual(2)
            .lessThanOrEqual(50))
        .constraint(Customer::email, "email", c -> c.notBlank()
            .email())
        
        // ネストされたオブジェクトのバリデーション
        .nest(Customer::address, "address", addressValidator)
        
        // コレクション要素のバリデーション
        .constraint(Customer::phoneNumbers, "phoneNumbers", c -> c.notEmpty()
            .lessThanOrEqual(3))
        .constraintOnEach(Customer::phoneNumbers, "phoneNumbers", c -> c.notBlank()
            .pattern("^0\\d{1,4}-\\d{1,4}-\\d{4}$")
            .message("電話番号は日本の形式で入力してください"))
        .build();
}

カスタムバリデータとメッセージ補間

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.constraint.CharSequenceConstraint;
import am.ik.yavi.core.CustomConstraint;
import am.ik.yavi.message.SimpleMessageFormatter;
import java.util.Locale;
import java.util.ResourceBundle;

public class CustomValidation {
    
    // カスタム制約の定義
    public static class JapanesePhoneNumberConstraint implements CustomConstraint<String> {
        @Override
        public boolean test(String value) {
            if (value == null) return true;
            return value.matches("^0\\d{1,4}-\\d{1,4}-\\d{4}$") ||
                   value.matches("^0\\d{9,10}$");
        }
        
        @Override
        public String defaultMessageFormat() {
            return "有効な日本の電話番号を入力してください";
        }
        
        @Override
        public String messageKey() {
            return "phoneNumber.japanese";
        }
    }
    
    // カスタム制約を使用したバリデータ
    public record ContactInfo(String phoneNumber, String mobileNumber) {}
    
    public static final Validator<ContactInfo> contactValidator = ValidatorBuilder.<ContactInfo>of()
        .constraint(ContactInfo::phoneNumber, "phoneNumber", c -> c
            .predicate(new JapanesePhoneNumberConstraint()))
        .constraint(ContactInfo::mobileNumber, "mobileNumber", c -> c
            .predicate(new JapanesePhoneNumberConstraint())
            .pattern("^0[789]0-\\d{4}-\\d{4}$")
            .message("携帯電話番号は090/080/070で始まる必要があります"))
        .build();
    
    // メッセージ補間のカスタマイズ
    public static void configureMessages() {
        // ResourceBundleベースのメッセージ
        ResourceBundle bundle = ResourceBundle.getBundle("ValidationMessages", Locale.JAPANESE);
        SimpleMessageFormatter formatter = new SimpleMessageFormatter(bundle::getString);
        
        Validator<User> userValidator = ValidatorBuilder.<User>of()
            .messageFormatter(formatter)
            .constraint(User::name, "name", c -> c.notBlank()
                .message("{field}は必須項目です"))
            .constraint(User::age, "age", c -> c.greaterThanOrEqual(20)
                .message("{field}は{value}以上である必要があります"))
            .build();
    }
}

ファクトリメソッドパターンでの使用

import am.ik.yavi.core.ConstraintViolations;
import am.ik.yavi.core.Validated;
import am.ik.yavi.fn.Either;

public class ValidatedFactory {
    
    // バリデーション済みオブジェクトの作成
    public record Email(String value) {
        private static final Validator<String> validator = ValidatorBuilder.<String>of()
            .constraint(s -> s, "email", c -> c.notBlank()
                .email()
                .lessThanOrEqual(255))
            .build();
        
        // ファクトリメソッド
        public static Either<ConstraintViolations, Email> of(String value) {
            return validator.validate(value)
                .map(Email::new);
        }
    }
    
    // より複雑な例:注文オブジェクト
    public record Order(
        String orderId,
        List<OrderItem> items,
        Double totalAmount
    ) {
        public record OrderItem(String productId, Integer quantity, Double price) {}
        
        private static final Validator<OrderItem> itemValidator = ValidatorBuilder.<OrderItem>of()
            .constraint(OrderItem::productId, "productId", c -> c.notBlank())
            .constraint(OrderItem::quantity, "quantity", c -> c.greaterThan(0))
            .constraint(OrderItem::price, "price", c -> c.greaterThan(0.0))
            .build();
        
        private static final Validator<Order> orderValidator = ValidatorBuilder.<Order>of()
            .constraint(Order::orderId, "orderId", c -> c.notBlank())
            .constraint(Order::items, "items", c -> c.notEmpty())
            .constraintOnEach(Order::items, "items", itemValidator)
            .constraint(Order::totalAmount, "totalAmount", c -> c.greaterThan(0.0))
            
            // 合計金額の整合性チェック
            .constraintOnTarget(order -> {
                double calculatedTotal = order.items.stream()
                    .mapToDouble(item -> item.quantity * item.price)
                    .sum();
                return Math.abs(calculatedTotal - order.totalAmount) < 0.01;
            }, "totalAmount", "totalAmount.mismatch", "合計金額が正しくありません")
            .build();
        
        // バリデーション付きファクトリメソッド
        public static Validated<Order> create(String orderId, List<OrderItem> items) {
            double total = items.stream()
                .mapToDouble(item -> item.quantity * item.price)
                .sum();
            
            Order order = new Order(orderId, items, total);
            return orderValidator.validate(order).fold(
                violations -> Validated.invalid(violations),
                validOrder -> Validated.valid(validOrder)
            );
        }
    }
}

Spring Boot統合

import am.ik.yavi.core.ConstraintViolations;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    private static final Validator<UserCreateRequest> createValidator = ValidatorBuilder.<UserCreateRequest>of()
        .constraint(UserCreateRequest::username, "username", c -> c.notBlank()
            .pattern("^[a-zA-Z0-9_]{3,20}$"))
        .constraint(UserCreateRequest::email, "email", c -> c.notBlank()
            .email())
        .constraint(UserCreateRequest::password, "password", c -> c.notBlank()
            .greaterThanOrEqual(8)
            .pattern("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$")
            .message("パスワードは大文字、小文字、数字を含む必要があります"))
        .build();
    
    @PostMapping
    public ResponseEntity<?> createUser(@RequestBody UserCreateRequest request) {
        return createValidator.validate(request)
            .fold(
                violations -> ResponseEntity.badRequest()
                    .body(toErrorResponse(violations)),
                validRequest -> {
                    User user = userService.create(validRequest);
                    return ResponseEntity.ok(user);
                }
            );
    }
    
    private Map<String, List<String>> toErrorResponse(ConstraintViolations violations) {
        return violations.stream()
            .collect(Collectors.groupingBy(
                ConstraintViolation::name,
                Collectors.mapping(ConstraintViolation::message, Collectors.toList())
            ));
    }
    
    // YAVIバリデーションアドバイス
    @RestControllerAdvice
    public class YaviValidationAdvice {
        
        @ExceptionHandler(ConstraintViolationsException.class)
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        public Map<String, Object> handleValidationException(ConstraintViolationsException e) {
            Map<String, Object> response = new HashMap<>();
            response.put("status", "error");
            response.put("message", "バリデーションエラー");
            response.put("errors", toErrorResponse(e.getViolations()));
            response.put("timestamp", LocalDateTime.now());
            return response;
        }
    }
}