Konform

KotlinValidationDSLMultiplatform

Konform

概要

Konformは、Kotlinマルチプラットフォーム対応のポータブルなバリデーションライブラリです。タイプセーフなDSLを提供し、読みやすく保守しやすいバリデーションルールの定義を可能にします。純粋なKotlinで実装されており、JVM、Android、JS、Nativeなど、あらゆるKotlinプラットフォームで動作します。

特徴

  • タイプセーフなDSL: プロパティ参照を使用した型安全なバリデーション定義
  • マルチプラットフォーム対応: Kotlin/JVM、Kotlin/JS、Kotlin/Native全てをサポート
  • 詳細なエラー情報: エラーパスとカスタムメッセージによる分かりやすいエラー報告
  • カスタムバリデーション: 独自のバリデーションルールを簡単に追加可能
  • コンテキストベースの検証: 動的な条件に基づくバリデーション
  • 再帰的検証: ネストされた構造や再帰的なデータ構造の検証
  • 軽量: 依存関係が少なく、パフォーマンスへの影響が最小限

インストール

kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation("io.konform:konform:0.11.0")
            }
        }
    }
}

使用例

基本的なバリデーション

import io.konform.validation.Validation
import io.konform.validation.Valid
import io.konform.validation.Invalid

data class UserProfile(
    val fullName: String,
    val email: String,
    val age: Int?
)

val validateUser = Validation<UserProfile> {
    UserProfile::fullName {
        minLength(2)
        maxLength(100)
    }
    
    UserProfile::email {
        pattern(".+@.+\\..+") hint "有効なメールアドレスを入力してください"
    }
    
    UserProfile::age ifPresent {
        minimum(0) hint "年齢は0以上である必要があります"
        maximum(150) hint "年齢は150以下である必要があります"
    }
}

// 使用例
val validUser = UserProfile("田中太郎", "[email protected]", 25)
val result = validateUser(validUser)

when (result) {
    is Valid -> println("バリデーション成功: ${result.value}")
    is Invalid -> {
        result.errors.forEach { error ->
            println("エラー: ${error.dataPath} - ${error.message}")
        }
    }
}

カスタムバリデーション

val validatePassword = Validation<String> {
    minLength(8) hint "パスワードは8文字以上必要です"
    
    // カスタム検証
    constrain("大文字を含む") { password ->
        password.any { it.isUpperCase() }
    }
    
    constrain("数字を含む") { password ->
        password.any { it.isDigit() }
    }
    
    constrain("特殊文字を含む") { password ->
        password.any { !it.isLetterOrDigit() }
    }
}

// パスワード強度チェック付きユーザー登録
data class UserRegistration(
    val username: String,
    val password: String,
    val confirmPassword: String
)

val validateRegistration = Validation<UserRegistration> {
    UserRegistration::username {
        minLength(3)
        maxLength(20)
        pattern("^[a-zA-Z0-9_]+$") hint "英数字とアンダースコアのみ使用可能"
    }
    
    UserRegistration::password {
        run(validatePassword)
    }
    
    // クロスフィールドバリデーション
    validate("パスワード確認") { registration ->
        registration.password == registration.confirmPassword
    } hint "パスワードが一致しません"
}

ネストされた構造の検証

data class Address(
    val street: String,
    val city: String,
    val postalCode: String,
    val country: String
)

data class Company(
    val name: String,
    val address: Address,
    val employees: List<UserProfile>
)

val validateAddress = Validation<Address> {
    Address::street {
        minLength(5)
    }
    
    Address::city {
        minLength(2)
    }
    
    Address::postalCode {
        pattern("\\d{3}-\\d{4}") hint "郵便番号は000-0000形式で入力してください"
    }
}

val validateCompany = Validation<Company> {
    Company::name {
        minLength(1)
        maxLength(100)
    }
    
    // ネストされたオブジェクトの検証
    Company::address {
        run(validateAddress)
    }
    
    // コレクションの検証
    Company::employees onEach {
        run(validateUser)
    }
    
    Company::employees {
        minSize(1) hint "少なくとも1人の従業員が必要です"
    }
}

コンテキストベースの検証

sealed class PaymentMethod
data class CreditCard(val number: String, val cvv: String) : PaymentMethod()
data class BankTransfer(val accountNumber: String) : PaymentMethod()
data class PayPal(val email: String) : PaymentMethod()

data class Order(
    val amount: Double,
    val paymentMethod: PaymentMethod
)

val validateOrder = Validation<Order> {
    Order::amount {
        minimum(0.01) hint "注文金額は0.01以上必要です"
    }
    
    // 動的な型チェックとバリデーション
    Order::paymentMethod validate { order ->
        when (val method = order.paymentMethod) {
            is CreditCard -> Validation<CreditCard> {
                CreditCard::number {
                    pattern("\\d{16}") hint "カード番号は16桁の数字です"
                }
                CreditCard::cvv {
                    pattern("\\d{3,4}") hint "CVVは3-4桁の数字です"
                }
            }(method)
            
            is BankTransfer -> Validation<BankTransfer> {
                BankTransfer::accountNumber {
                    pattern("\\d{7,14}") hint "口座番号は7-14桁の数字です"
                }
            }(method)
            
            is PayPal -> Validation<PayPal> {
                PayPal::email {
                    pattern(".+@.+\\..+") hint "有効なメールアドレスを入力してください"
                }
            }(method)
        }
    }
}

エラーの処理とカスタマイズ

enum class Severity {
    ERROR, WARNING, INFO
}

val validateWithSeverity = Validation<UserProfile> {
    UserProfile::fullName {
        minLength(2) userContext Severity.ERROR
    }
    
    UserProfile::email {
        pattern(".+@.+\\..+").replace(
            hint = "メールアドレスの形式が正しくありません",
            userContext = Severity.ERROR
        )
    }
    
    UserProfile::age ifPresent {
        validate("年齢の推奨範囲") { it in 18..65 }
            .hint("このサービスは18-65歳の方に最適化されています")
            .userContext(Severity.WARNING)
    }
}

// エラー処理
val result = validateWithSeverity(user)
if (result is Invalid) {
    val errors = result.errors.filter { 
        it.userContext == Severity.ERROR 
    }
    val warnings = result.errors.filter { 
        it.userContext == Severity.WARNING 
    }
    
    if (errors.isNotEmpty()) {
        println("エラー:")
        errors.forEach { println("  - ${it.dataPath}: ${it.message}") }
    }
    
    if (warnings.isNotEmpty()) {
        println("警告:")
        warnings.forEach { println("  - ${it.dataPath}: ${it.message}") }
    }
}

フェイルファースト検証

// 高コストな検証を後に実行
val quickValidation = Validation<String> {
    minLength(8)
    maxLength(100)
}

val expensiveValidation = Validation<String> {
    constrain("データベースチェック") { value ->
        // データベースアクセスなど重い処理
        checkDatabase(value)
    }
}

val efficientValidation = Validation<String> {
    // quickValidationが成功した場合のみexpensiveValidationを実行
    run(quickValidation andThen expensiveValidation)
}

テストでの使用

import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.konform.validation.Invalid
import io.konform.validation.Valid

class UserValidationTest {
    @Test
    fun `有効なユーザーはバリデーションに成功する`() {
        val user = UserProfile("山田花子", "[email protected]", 30)
        val result = validateUser(user)
        
        result shouldBe Valid(user)
    }
    
    @Test
    fun `無効なメールアドレスはエラーになる`() {
        val user = UserProfile("山田花子", "invalid-email", 30)
        val result = validateUser(user)
        
        result should { it is Invalid }
        (result as Invalid).errors.any { 
            it.dataPath == ".email" 
        } shouldBe true
    }
}

比較・代替手段

類似ライブラリとの比較

  • Valiktor: より多くの組み込みバリデーターを提供するが、開発が停滞
  • Arrow Validated: 関数型プログラミング寄りで、Arrowエコシステムとの統合が前提
  • Android Form Validators: Android専用でUI統合に特化

Konformを選ぶべき場合

  • Kotlinマルチプラットフォームプロジェクト
  • シンプルで読みやすいDSLを重視
  • 軽量なライブラリを求める場合
  • カスタムバリデーションの柔軟性が必要

学習リソース

まとめ

KonformはKotlinエコシステムにおける優れたバリデーションライブラリです。タイプセーフなDSL、マルチプラットフォーム対応、詳細なエラー報告により、あらゆるKotlinプロジェクトでデータ検証を簡潔かつ効果的に実装できます。特にKotlinマルチプラットフォームプロジェクトでは、統一されたバリデーションロジックを全プラットフォームで共有できる点が大きな利点となります。