Valiktor
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の構築に最適です。豊富な組み込みバリデーターと拡張性により、シンプルなデータ検証から複雑なビジネスルールまで、あらゆるバリデーションニーズに対応できます。