KotlinX Validation - Kotlin公式バリデーションライブラリ(仮想)

JetBrainsが提供する予定のKotlin公式バリデーションライブラリ。kotlinx.serializationとの統合により、シリアライゼーション時の自動バリデーションを実現。

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Callout } from '@/components/callout' import CodeBlock from '@/components/code-block'

概要

このライブラリは現在構想段階にあり、実際にはリリースされていません。以下の内容は、Kotlinコミュニティで議論されている理想的なバリデーションライブラリの仕様を基にした仮想的な実装例です。

KotlinX Validationは、JetBrainsが開発を検討している公式のKotlinバリデーションライブラリです。kotlinx.serializationとの深い統合により、データのシリアライゼーション・デシリアライゼーション時に自動的にバリデーションを実行できることを目指しています。

主な特徴

  • kotlinx.serialization統合: シリアライゼーション時の自動バリデーション
  • コルーチン対応: 非同期バリデーションのネイティブサポート
  • 型安全なDSL: Kotlinの型システムを活用した安全なバリデーション定義
  • マルチプラットフォーム: JVM、JS、Nativeすべてで動作
  • カスタムバリデータ: 独自のバリデーションロジックの簡単な実装
  • エラーアキュムレーション: すべてのバリデーションエラーを収集して返却

インストール(仮想)

Gradle (Kotlin) Gradle (Groovy) Maven {`dependencies { implementation("org.jetbrains.kotlinx:kotlinx-validation:0.1.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") }`} {`dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-validation:0.1.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' }`} {` org.jetbrains.kotlinx kotlinx-validation 0.1.0 org.jetbrains.kotlinx kotlinx-serialization-json 1.6.0 `}

基本的な使い方

シンプルなバリデーション

{`import kotlinx.serialization.Serializable import kotlinx.validation.*

@Serializable @Validated data class User( @field:NotBlank @field:Length(min = 3, max = 20) val username: String,

@field:Email
val email: String,

@field:Range(min = 18, max = 120)
val age: Int,

@field:Pattern("^\\+?[1-9]\\d{1,14}$")
val phoneNumber: String?

)

// 使用例 fun main() { val user = User( username = "john_doe", email = "[email protected]", age = 25, phoneNumber = "+1234567890" )

// 自動バリデーション
val validationResult = validate(user)

if (validationResult.isValid) {
    println("ユーザーデータは有効です")
} else {
    validationResult.errors.forEach { error ->
        println("エラー: ${error.field} - ${error.message}")
    }
}

}`}

kotlinx.serializationとの統合

{`import kotlinx.serialization.json.Json import kotlinx.validation.ValidatingJson

// バリデーション機能付きJsonインスタンス val validatingJson = ValidatingJson { prettyPrint = true ignoreUnknownKeys = true validateOnDeserialize = true }

// JSONからのデシリアライズ時に自動バリデーション fun parseUserJson(jsonString: String): User? { return try { validatingJson.decodeFromString(jsonString) } catch (e: ValidationException) { println("バリデーションエラー:") e.errors.forEach { error -> println(" ${error.field}: ${error.message}") } null } }

// 使用例 val jsonInput = """ { "username": "ab", "email": "invalid-email", "age": 150, "phoneNumber": "invalid" } """

val user = parseUserJson(jsonInput) // 出力: // バリデーションエラー: // username: 長さは3文字以上である必要があります // email: 有効なメールアドレスの形式である必要があります // age: 値は120以下である必要があります // phoneNumber: 指定されたパターンに一致する必要があります`}

カスタムバリデータ

同期バリデータ

{`import kotlinx.validation.*

// カスタムバリデータアノテーション @Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.RUNTIME) @Constraint(validatedBy = [UniqueUsernameValidator::class]) annotation class UniqueUsername( val message: String = "ユーザー名は既に使用されています" )

// バリデータ実装 class UniqueUsernameValidator : ConstraintValidator<UniqueUsername, String> { override fun isValid(value: String?, context: ConstraintValidatorContext): Boolean { if (value == null) return true

    // データベースチェックのシミュレーション
    val existingUsernames = setOf("admin", "user", "test")
    return !existingUsernames.contains(value.lowercase())
}

}

// 使用例 @Serializable @Validated data class NewUser( @field:UniqueUsername @field:NotBlank val username: String )`}

非同期バリデータ(コルーチン対応)

{`import kotlinx.coroutines.* import kotlinx.validation.*

// 非同期バリデータアノテーション @Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.RUNTIME) @AsyncConstraint(validatedBy = [EmailAvailabilityValidator::class]) annotation class EmailAvailable( val message: String = "このメールアドレスは既に登録されています" )

// 非同期バリデータ実装 class EmailAvailabilityValidator : AsyncConstraintValidator<EmailAvailable, String> { override suspend fun isValid( value: String?, context: ConstraintValidatorContext ): Boolean { if (value == null) return true

    // 外部APIチェックのシミュレーション
    return withContext(Dispatchers.IO) {
        delay(100) // API呼び出しのシミュレーション
        checkEmailInDatabase(value)
    }
}

private suspend fun checkEmailInDatabase(email: String): Boolean {
    // 実際のデータベースチェック
    val registeredEmails = setOf("[email protected]", "[email protected]")
    return !registeredEmails.contains(email.lowercase())
}

}

// 使用例 @Serializable @Validated data class Registration( @field:Email @field:EmailAvailable val email: String,

@field:NotBlank
@field:Length(min = 8)
val password: String

)

// 非同期バリデーション実行 suspend fun registerUser(registration: Registration) { val result = validateAsync(registration)

if (result.isValid) {
    println("登録成功")
} else {
    result.errors.forEach { error ->
        println("エラー: ${error.field} - ${error.message}")
    }
}

}`}

条件付きバリデーション

{`import kotlinx.validation.*

@Serializable @Validated data class PaymentMethod( val type: PaymentType,

@field:ValidateIf("type == PaymentType.CREDIT_CARD")
@field:CreditCardNumber
val cardNumber: String?,

@field:ValidateIf("type == PaymentType.BANK_TRANSFER")
@field:IBAN
val iban: String?,

@field:ValidateIf("type == PaymentType.PAYPAL")
@field:Email
val paypalEmail: String?

)

enum class PaymentType { CREDIT_CARD, BANK_TRANSFER, PAYPAL }

// 使用例 fun validatePayment() { val creditCardPayment = PaymentMethod( type = PaymentType.CREDIT_CARD, cardNumber = "4111111111111111", iban = null, paypalEmail = null )

val result = validate(creditCardPayment)
// cardNumberのみがバリデーションされる

}`}

グループバリデーション

{`import kotlinx.validation.*

// バリデーショングループ interface BasicInfo interface DetailedInfo interface AdminOnly

@Serializable @Validated data class UserProfile( @field:NotBlank(groups = [BasicInfo::class]) val name: String,

@field:Email(groups = [BasicInfo::class])
val email: String,

@field:PhoneNumber(groups = [DetailedInfo::class])
val phone: String?,

@field:Address(groups = [DetailedInfo::class])
val address: String?,

@field:NotNull(groups = [AdminOnly::class])
val adminNotes: String?

)

// グループ指定でバリデーション実行 fun validateByGroup(profile: UserProfile) { // 基本情報のみバリデーション val basicResult = validate(profile, groups = setOf(BasicInfo::class))

// 詳細情報を含めてバリデーション
val detailedResult = validate(
    profile, 
    groups = setOf(BasicInfo::class, DetailedInfo::class)
)

// 管理者用バリデーション
val adminResult = validate(
    profile,
    groups = setOf(BasicInfo::class, DetailedInfo::class, AdminOnly::class)
)

}`}

Flowを使用したリアルタイムバリデーション

{`import kotlinx.coroutines.flow.* import kotlinx.validation.*

class UserInputValidator { // リアルタイムバリデーション用のFlow fun validateEmailFlow(emailFlow: Flow): Flow { return emailFlow .debounce(300) // 入力が止まってから300ms待つ .map { email -> val validator = EmailValidator() ValidationResult( field = "email", value = email, errors = if (validator.isValid(email)) { emptyList() } else { listOf(ValidationError( field = "email", message = "有効なメールアドレスを入力してください" )) } ) } }

// 複数フィールドの同時バリデーション
fun validateFormFlow(
    usernameFlow: Flow<String>,
    emailFlow: Flow<String>,
    passwordFlow: Flow<String>
): Flow<FormValidationState> {
    return combine(
        usernameFlow.debounce(300),
        emailFlow.debounce(300),
        passwordFlow.debounce(300)
    ) { username, email, password ->
        FormValidationState(
            usernameErrors = validateUsername(username),
            emailErrors = validateEmail(email),
            passwordErrors = validatePassword(password),
            isValid = validateUsername(username).isEmpty() &&
                     validateEmail(email).isEmpty() &&
                     validatePassword(password).isEmpty()
        )
    }
}

private fun validateUsername(username: String): List<String> {
    val errors = mutableListOf<String>()
    if (username.length < 3) errors.add("ユーザー名は3文字以上必要です")
    if (username.length > 20) errors.add("ユーザー名は20文字以下にしてください")
    if (!username.matches(Regex("^[a-zA-Z0-9_]+$"))) {
        errors.add("ユーザー名は英数字とアンダースコアのみ使用できます")
    }
    return errors
}

private fun validateEmail(email: String): List<String> {
    val errors = mutableListOf<String>()
    if (!email.matches(Regex("^[^@]+@[^@]+\\.[^@]+$"))) {
        errors.add("有効なメールアドレスを入力してください")
    }
    return errors
}

private fun validatePassword(password: String): List<String> {
    val errors = mutableListOf<String>()
    if (password.length < 8) errors.add("パスワードは8文字以上必要です")
    if (!password.any { it.isUpperCase() }) {
        errors.add("大文字を1文字以上含めてください")
    }
    if (!password.any { it.isLowerCase() }) {
        errors.add("小文字を1文字以上含めてください")
    }
    if (!password.any { it.isDigit() }) {
        errors.add("数字を1文字以上含めてください")
    }
    return errors
}

}

data class FormValidationState( val usernameErrors: List, val emailErrors: List, val passwordErrors: List, val isValid: Boolean )`}

エラーハンドリングとカスタマイズ

{`import kotlinx.validation.*

// カスタムエラーメッセージ @Serializable @Validated data class Product( @field:NotBlank(message = "商品名は必須です") @field:Length( min = 3, max = 100, message = "商品名は{min}文字以上{max}文字以下で入力してください" ) val name: String,

@field:Positive(message = "価格は0より大きい値を設定してください")
@field:Max(value = 1000000, message = "価格は{value}円以下で設定してください")
val price: Double,

@field:Min(value = 0, message = "在庫数は0以上である必要があります")
val stock: Int

)

// エラーメッセージのローカライゼーション class JapaneseValidationMessageProvider : ValidationMessageProvider { override fun getMessage( constraint: Constraint, params: Map<String, Any> ): String { return when (constraint) { is NotBlank -> "必須項目です" is Email -> "有効なメールアドレスを入力してください" is Length -> { val min = params["min"] as? Int ?: 0 val max = params["max"] as? Int ?: Int.MAX_VALUE "${min}文字以上${max}文字以下で入力してください" } is Range -> { val min = params["min"] as? Number ?: 0 val max = params["max"] as? Number ?: Int.MAX_VALUE "${min}以上${max}以下の値を入力してください" } else -> "入力値が無効です" } } }

// カスタムエラーハンドラー class ValidationErrorHandler { fun handleErrors(errors: List): Map<String, List> { return errors.groupBy { it.field } .mapValues { (_, fieldErrors) -> fieldErrors.map { it.message } } }

fun toUserFriendlyMessage(errors: List<ValidationError>): String {
    return buildString {
        appendLine("入力内容に以下の問題があります:")
        errors.forEach { error ->
            appendLine("・${error.field}: ${error.message}")
        }
    }
}

}`}

ベストプラクティス

1. バリデーションの分離

{`// バリデーションロジックを専用クラスに分離 class UserValidationRules { companion object { val USERNAME = ValidationRule { notBlank() length(3..20) pattern("^[a-zA-Z0-9_]+$") custom { value -> if (value.lowercase() in RESERVED_USERNAMES) { error("このユーザー名は使用できません") } } }
    val EMAIL = ValidationRule<String> {
        notBlank()
        email()
        maxLength(255)
    }
    
    val PASSWORD = ValidationRule<String> {
        notBlank()
        minLength(8)
        containsUppercase()
        containsLowercase()
        containsDigit()
        containsSpecialChar()
    }
    
    private val RESERVED_USERNAMES = setOf(
        "admin", "root", "system", "user", "test"
    )
}

}

// 使用例 @Serializable @Validated data class UserRegistration( @field:ValidatedBy(UserValidationRules.USERNAME) val username: String,

@field:ValidatedBy(UserValidationRules.EMAIL)
val email: String,

@field:ValidatedBy(UserValidationRules.PASSWORD)
val password: String

)`}

2. テスト可能なバリデーション

{`import kotlin.test.*

class UserValidationTest { private val validator = Validator()

@Test
fun `有効なユーザーデータはバリデーションを通過する`() {
    val user = User(
        username = "john_doe",
        email = "[email protected]",
        age = 25,
        phoneNumber = "+1234567890"
    )
    
    val result = validator.validate(user)
    assertTrue(result.isValid)
    assertTrue(result.errors.isEmpty())
}

@Test
fun `無効なメールアドレスはエラーになる`() {
    val user = User(
        username = "john_doe",
        email = "invalid-email",
        age = 25,
        phoneNumber = null
    )
    
    val result = validator.validate(user)
    assertFalse(result.isValid)
    
    val emailError = result.errors.find { it.field == "email" }
    assertNotNull(emailError)
    assertEquals("有効なメールアドレスの形式である必要があります", emailError.message)
}

@Test
fun `複数のバリデーションエラーが収集される`() {
    val user = User(
        username = "ab", // 短すぎる
        email = "invalid", // 無効な形式
        age = 150, // 範囲外
        phoneNumber = "invalid" // パターン不一致
    )
    
    val result = validator.validate(user)
    assertFalse(result.isValid)
    assertEquals(4, result.errors.size)
}

}`}

3. パフォーマンス最適化

{`// バリデーションのキャッシング class CachedValidator { private val cache = mutableMapOf()
fun validate(value: T, validator: Validator<T>): ValidationResult {
    return cache.getOrPut(value) {
        validator.validate(value)
    }
}

fun invalidate(value: T) {
    cache.remove(value)
}

fun clear() {
    cache.clear()
}

}

// 遅延バリデーション @Serializable @Validated data class LazyValidatedData( @field:LazyValidation val expensiveToValidate: String,

@field:EagerValidation
val mustValidateImmediately: String

)

// バッチバリデーション suspend fun validateBatch(items: List): BatchValidationResult { return coroutineScope { val results = items.map { item -> async { item to validateAsync(item) } }.awaitAll()

    BatchValidationResult(
        results = results.toMap(),
        allValid = results.all { it.second.isValid },
        errorCount = results.sumOf { it.second.errors.size }
    )
}

}`}

まとめ

KotlinX Validationは、Kotlinエコシステムに完全に統合された強力なバリデーションライブラリとして設計されています。kotlinx.serializationとの深い統合、コルーチンによる非同期バリデーションのサポート、型安全なDSLによる直感的なAPI設計により、モダンなKotlinアプリケーションのバリデーションニーズに対応します。

このドキュメントは、Kotlinコミュニティで議論されている理想的なバリデーションライブラリの仕様を基にした仮想的な実装例です。実際の開発では、Konform、Valiktor、Akkurateなどの既存のライブラリの使用を検討してください。

関連リンク

  • kotlinx.serialization - Kotlin公式シリアライゼーションライブラリ
  • Konform - 軽量なKotlinバリデーションライブラリ
  • Valiktor - 型安全なDSLを持つバリデーションライブラリ
  • Akkurate - 非同期データソース対応のバリデーションライブラリ