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'
概要
KotlinX Validationは、JetBrainsが開発を検討している公式のKotlinバリデーションライブラリです。kotlinx.serializationとの深い統合により、データのシリアライゼーション・デシリアライゼーション時に自動的にバリデーションを実行できることを目指しています。
主な特徴
- kotlinx.serialization統合: シリアライゼーション時の自動バリデーション
- コルーチン対応: 非同期バリデーションのネイティブサポート
- 型安全なDSL: Kotlinの型システムを活用した安全なバリデーション定義
- マルチプラットフォーム: JVM、JS、Nativeすべてで動作
- カスタムバリデータ: 独自のバリデーションロジックの簡単な実装
- エラーアキュムレーション: すべてのバリデーションエラーを収集して返却
インストール(仮想)
基本的な使い方
シンプルなバリデーション
@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との統合
// バリデーション機能付きJsonインスタンス val validatingJson = ValidatingJson { prettyPrint = true ignoreUnknownKeys = true validateOnDeserialize = true }
// JSONからのデシリアライズ時に自動バリデーション
fun parseUserJson(jsonString: String): User? {
return try {
validatingJson.decodeFromString
// 使用例 val jsonInput = """ { "username": "ab", "email": "invalid-email", "age": 150, "phoneNumber": "invalid" } """
val user = parseUserJson(jsonInput) // 出力: // バリデーションエラー: // username: 長さは3文字以上である必要があります // email: 有効なメールアドレスの形式である必要があります // age: 値は120以下である必要があります // phoneNumber: 指定されたパターンに一致する必要があります`}
カスタムバリデータ
同期バリデータ
// カスタムバリデータアノテーション @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 )`}
非同期バリデータ(コルーチン対応)
// 非同期バリデータアノテーション @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}")
}
}
}`}
条件付きバリデーション
@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のみがバリデーションされる
}`}
グループバリデーション
// バリデーショングループ 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を使用したリアルタイムバリデーション
class UserInputValidator {
// リアルタイムバリデーション用のFlow
fun validateEmailFlow(emailFlow: Flow
// 複数フィールドの同時バリデーション
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
エラーハンドリングとカスタマイズ
// カスタムエラーメッセージ @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
fun toUserFriendlyMessage(errors: List<ValidationError>): String {
return buildString {
appendLine("入力内容に以下の問題があります:")
errors.forEach { error ->
appendLine("・${error.field}: ${error.message}")
}
}
}
}`}
ベストプラクティス
1. バリデーションの分離
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. テスト可能なバリデーション
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. パフォーマンス最適化
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(
results = results.toMap(),
allValid = results.all { it.second.isValid },
errorCount = results.sumOf { it.second.errors.size }
)
}
}`}
まとめ
KotlinX Validationは、Kotlinエコシステムに完全に統合された強力なバリデーションライブラリとして設計されています。kotlinx.serializationとの深い統合、コルーチンによる非同期バリデーションのサポート、型安全なDSLによる直感的なAPI設計により、モダンなKotlinアプリケーションのバリデーションニーズに対応します。
関連リンク
- kotlinx.serialization - Kotlin公式シリアライゼーションライブラリ
- Konform - 軽量なKotlinバリデーションライブラリ
- Valiktor - 型安全なDSLを持つバリデーションライブラリ
- Akkurate - 非同期データソース対応のバリデーションライブラリ