Valiktor

KotlinValidationDSLSpring Booti18n

Valiktor

概要

Valiktorは、Kotlin向けのタイプセーフで強力かつ拡張可能な流暢なDSLを提供するバリデーションライブラリです。直感的なAPIにより、オブジェクトの検証を簡潔に記述でき、Spring Bootとの統合や国際化(i18n)対応により、エンタープライズグレードのアプリケーション開発をサポートします。

特徴

  • タイプセーフなDSL: Kotlinのプロパティ参照を使用した型安全なバリデーション
  • 豊富な組み込みバリデーター: 40以上の制約と200以上のバリデーション関数を提供
  • Spring Boot統合: Spring WebMvcとWebFluxの両方をサポート
  • 完全な国際化対応: 多言語対応のエラーメッセージシステム
  • カスタマイズ可能: 独自のバリデーターとフォーマッターの作成が容易
  • 詳細なエラー情報: プロパティパスと値を含む詳細なエラーレポート
  • モジュール設計: 必要な機能のみを選択して使用可能

インストール

Core モジュール

// build.gradle.kts
dependencies {
    implementation("org.valiktor:valiktor-core:0.12.0")
}

Spring Boot Starter

dependencies {
    implementation("org.valiktor:valiktor-spring-boot-starter:0.12.0")
}

その他のモジュール

dependencies {
    // Java Time API サポート
    implementation("org.valiktor:valiktor-javatime:0.12.0")
    
    // Joda-Money サポート
    implementation("org.valiktor:valiktor-javamoney:0.12.0")
    
    // テストユーティリティ
    testImplementation("org.valiktor:valiktor-test:0.12.0")
}

使用例

基本的なバリデーション

import org.valiktor.validate
import org.valiktor.functions.*

data class Employee(
    val id: Int,
    val name: String,
    val email: String,
    val salary: Double?
) {
    init {
        validate(this) {
            validate(Employee::id).isPositive()
            validate(Employee::name)
                .isNotBlank()
                .hasSize(min = 3, max = 80)
            validate(Employee::email)
                .isNotBlank()
                .isEmail()
            validate(Employee::salary)
                .isPositiveOrZero()
        }
    }
}

// 使用例
try {
    val employee = Employee(
        id = -1,
        name = "太",
        email = "invalid-email",
        salary = -1000.0
    )
} catch (ex: ConstraintViolationException) {
    ex.constraintViolations.forEach { violation ->
        println("${violation.property}: ${violation.constraint}")
    }
}

ネストされたオブジェクトの検証

data class Company(val name: String)
data class City(val name: String)

data class Address(
    val street: String,
    val city: City,
    val zipCode: String
)

data class Employee(
    val id: Int,
    val name: String,
    val company: Company,
    val address: Address
)

// ネストされた検証
validate(employee) {
    validate(Employee::id).isPositive()
    validate(Employee::name).isNotEmpty()
    
    // 会社の検証
    validate(Employee::company).validate {
        validate(Company::name).isNotEmpty()
    }
    
    // 住所の検証(深いネスト)
    validate(Employee::address).validate {
        validate(Address::street).hasSize(min = 5, max = 255)
        validate(Address::zipCode).matches("\\d{3}-\\d{4}")
        
        validate(Address::city).validate {
            validate(City::name).isNotEmpty()
        }
    }
}

コレクションの検証

data class Team(
    val name: String,
    val members: List<Employee>,
    val tags: Set<String>
)

validate(team) {
    validate(Team::name).isNotBlank()
    
    // リストの各要素を検証
    validate(Team::members)
        .isNotEmpty()
        .hasSize(max = 10)
    
    validate(Team::members).validateForEach {
        validate(Employee::id).isPositive()
        validate(Employee::name).isNotEmpty()
        validate(Employee::email).isEmail()
    }
    
    // セットの検証
    validate(Team::tags)
        .isNotEmpty()
        .validateForEach {
            isNotBlank()
            hasSize(min = 2, max = 20)
        }
}

カスタムバリデーション

import org.valiktor.Constraint
import org.valiktor.Validator

// カスタム制約の定義
data class Between<T>(val start: T, val end: T) : Constraint {
    override val name = "Between"
}

// 拡張関数の作成
fun <E> Validator<E>.Property<Int?>.isBetween(start: Int, end: Int) =
    this.validate(Between(start, end)) { 
        it == null || it in start..end 
    }

// カスタム電話番号バリデーション
data class JapanesePhoneNumber(val message: String = "有効な日本の電話番号ではありません") : Constraint

fun <E> Validator<E>.Property<String?>.isJapanesePhoneNumber() =
    this.validate(JapanesePhoneNumber()) { value ->
        value == null || value.matches(Regex("^0\\d{1,4}-\\d{1,4}-\\d{4}$"))
    }

// 使用例
data class Contact(
    val name: String,
    val age: Int,
    val phoneNumber: String
)

validate(contact) {
    validate(Contact::name).isNotBlank()
    validate(Contact::age).isBetween(0, 150)
    validate(Contact::phoneNumber).isJapanesePhoneNumber()
}

Spring Boot統合

設定

# application.yml
valiktor:
  base-bundle-name: messages

REST API での使用

@RestController
@RequestMapping("/api/employees")
class EmployeeController {
    
    @PostMapping
    fun createEmployee(@RequestBody employee: Employee): ResponseEntity<Employee> {
        // Employeeクラスのinitブロックで自動的にバリデーションが実行される
        // バリデーションエラーは自動的に422 Unprocessable Entityとして返される
        return ResponseEntity.ok(employee)
    }
}

// カスタムバリデーションサービス
@Service
class EmployeeValidationService {
    
    fun validateForUpdate(employee: Employee, existingId: Int) {
        validate(employee) {
            validate(Employee::id).isEqualTo(existingId)
            validate(Employee::email).isEmail()
            
            // ビジネスルールに基づく検証
            validate(Employee::salary).validate { salary ->
                salary == null || salary >= getMinimumSalaryForRole(employee.role)
            }
        }
    }
}

国際化(i18n)

基本的な使用方法

import org.valiktor.i18n.mapToMessage
import java.util.Locale

try {
    validate(employee) {
        validate(Employee::id).isPositive()
        validate(Employee::name).isNotEmpty()
        validate(Employee::email).isEmail()
    }
} catch (ex: ConstraintViolationException) {
    // 日本語でメッセージを取得
    val japaneseMessages = ex.constraintViolations
        .mapToMessage(locale = Locale.JAPANESE)
        .map { "${it.property}: ${it.message}" }
    
    japaneseMessages.forEach(::println)
    // 出力例:
    // id: 1より大きい値にする必要があります
    // name: 空にすることはできません
    // email: 有効なメールアドレスではありません
}

カスタムメッセージの定義

# messages_ja.properties
org.valiktor.constraints.NotEmpty.message=必須項目です
org.valiktor.constraints.Email.message=正しいメールアドレスを入力してください
org.valiktor.constraints.Size.message.min=最小{min}文字必要です
org.valiktor.constraints.Size.message.max=最大{max}文字までです
org.valiktor.constraints.Size.message.min.max={min}文字以上{max}文字以下で入力してください

# カスタム制約のメッセージ
com.example.constraints.JapanesePhoneNumber.message=有効な日本の電話番号(例:03-1234-5678)を入力してください

カスタムフォーマッター

import org.valiktor.i18n.Formatter
import org.valiktor.i18n.MessageBundle
import java.time.LocalDate
import java.time.format.DateTimeFormatter

// 日本の日付フォーマッター
object JapaneseDateFormatter : Formatter<LocalDate> {
    private val formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日")
    
    override fun format(value: LocalDate, messageBundle: MessageBundle): String {
        return value.format(formatter)
    }
}

// フォーマッターの登録
// META-INF/services/org.valiktor.i18n.FormatterSpi に登録

条件付きバリデーション

data class User(
    val type: UserType,
    val email: String?,
    val phoneNumber: String?,
    val companyName: String?
)

enum class UserType {
    INDIVIDUAL, CORPORATE
}

validate(user) {
    // 共通のバリデーション
    validate(User::type).isNotNull()
    
    // タイプに応じた条件付きバリデーション
    when (user.type) {
        UserType.INDIVIDUAL -> {
            validate(User::email).isNotNull().isEmail()
            validate(User::phoneNumber).isNotNull()
            validate(User::companyName).isNull()
        }
        UserType.CORPORATE -> {
            validate(User::companyName).isNotNull().hasSize(min = 2, max = 100)
            validate(User::email).isNotNull().isEmail()
        }
    }
}

テストでの使用

import org.valiktor.test.shouldFailValidation
import org.valiktor.test.shouldNotFailValidation
import org.junit.jupiter.api.Test

class EmployeeValidationTest {
    
    @Test
    fun `有効な従業員データは検証に合格する`() {
        val employee = Employee(
            id = 1,
            name = "山田太郎",
            email = "[email protected]",
            salary = 300000.0
        )
        
        employee.shouldNotFailValidation()
    }
    
    @Test
    fun `無効なメールアドレスは検証に失敗する`() {
        val employee = Employee(
            id = 1,
            name = "山田太郎",
            email = "invalid-email",
            salary = 300000.0
        )
        
        employee.shouldFailValidation {
            expect(Employee::email, Email)
        }
    }
}

サポートされているバリデーション関数

汎用

  • isNull(), isNotNull()
  • isEqualTo(), isNotEqualTo()
  • isIn(), isNotIn()
  • isValid(), isNotValid()

文字列

  • isBlank(), isNotBlank()
  • isEmpty(), isNotEmpty()
  • hasSize(), matches()
  • isEmail(), isWebsite()
  • startsWith(), endsWith()
  • contains(), doesNotContain()

数値

  • isZero(), isNotZero()
  • isPositive(), isNegative()
  • isPositiveOrZero(), isNegativeOrZero()
  • isBetween(), isNotBetween()
  • isLessThan(), isGreaterThan()

コレクション

  • isEmpty(), isNotEmpty()
  • hasSize(), contains()
  • doesNotContain(), containsAll()

日付・時刻(JavaTimeモジュール)

  • isBefore(), isAfter()
  • isBetween(), isToday()
  • isPast(), isFuture()

比較・代替手段

類似ライブラリとの比較

  • Konform: より軽量でKotlinマルチプラットフォーム対応だが、機能は限定的
  • Hibernate Validator: Java標準(Bean Validation)準拠だが、Kotlin DSLの表現力に欠ける
  • Arrow Validation: 関数型プログラミング指向で、学習曲線が急

Valiktorを選ぶべき場合

  • Kotlin専用の表現力豊かなDSLを求める場合
  • Spring Bootとの密な統合が必要な場合
  • 多言語対応のエンタープライズアプリケーション
  • 詳細なエラー情報とカスタマイズ性を重視する場合

学習リソース

まとめ

ValiktorはKotlinのための包括的なバリデーションソリューションです。タイプセーフなDSL、Spring Bootとの優れた統合、完全な国際化サポートにより、堅牢な多言語RESTful APIの構築に最適です。豊富な組み込みバリデーターと拡張性により、シンプルなデータ検証から複雑なビジネスルールまで、あらゆるバリデーションニーズに対応できます。