Data validation is one of the most critical aspects of any application, yet it’s often the source of repetitive, untestable, and unreadable code. In the Kotlin world, where conciseness and expressiveness are king, we need a better solution than bulky frameworks or error-prone manual checks.
Enter Konform, the powerful, type-safe, and multiplatform-ready validation library designed specifically for Kotlin. Konform empowers you to define complex validation rules with an elegant Domain Specific Language (DSL), making your validation logic look like clear, executable documentation.
🌟 Konform’s Core Strengths
1. The Power of a Type-Safe DSL
dependencies {
implementation("io.konform:konform-jvm:0.11.0")
}
Konform’s greatest feature is its idiomatic use of the Kotlin DSL. Instead of relying on string-based property names or annotations that break easily with refactoring, Konform uses Kotlin property references (::) for total type-safety.
This means you get compile-time safety for your validation paths. Rename a field in your data class, and the Konform validator will instantly show a compiler error, saving you from runtime bugs.
Example: Basic Validation
data class UserProfile(val fullName: String, val age: Int?)
val validateUser = Validation<UserProfile> {
// Access properties directly via ::
UserProfile::fullName {
minLength(2)
maxLength(100)
pattern("^[a-zA-Z\\s]+$") // Ensure only letters and spaces
}
// 'ifPresent' handles nullable fields gracefully
UserProfile::age ifPresent {
minimum(0)
maximum(150)
}
}
2. Comprehensive Error Handling
Konform’s goal is to provide complete feedback to the user in a single pass. The result of a validation is a sealed class, giving you a clean way to handle success or failure:
Valid(value: T): The data passed all checks.Invalid(errors: Map<String, List<String>>): A map containing all field paths (as strings) and a list of corresponding error messages.
Example: Applying and Accessing Results
val invalidUser = UserProfile("A", -5)
val validationResult = validateUser(invalidUser)
when (validationResult) {
is Valid -> println("Data is valid: ${validationResult.value}")
is Invalid -> {
println("Validation failed with errors:")
// Access errors using map-like indexing
println("Full Name Errors: ${validationResult["fullName"]}")
println("Age Errors: ${validationResult["age"]}")
}
}
// Output:
// Full Name Errors: [must be at least 2 characters long]
// Age Errors: [must be at least 0]
🧬 Advanced Validation for Real-World Scenarios
Konform truly shines when dealing with complex data models: nested objects, collections, and conditional logic.
1. Nested and Collection Validation
You can easily reuse validations and apply rules to every element within a collection.
data class Address(val street: String, val city: String, val postalCode: String)
data class Customer(val name: String, val addresses: List<Address>)
val validateAddress = Validation<Address> {
Address::street { notBlank() }
Address::city { notBlank() }
}
val validateCustomer = Validation<Customer> {
Customer::name { minLength(2) }
// Apply an existing validation to every item in the list
Customer::addresses onEach {
run(validateAddress)
}
}
2. Dynamic and Conditional Constraints
Need a field’s validation to depend on another field? Konform’s dynamic block and the ability to inject custom logic (constrain) make this simple.
data class Order(val type: String, val expressShipping: Boolean, val deliveryTime: Int)
val validateOrder = Validation<Order> {
Order::deliveryTime dynamic { order ->
if (order.expressShipping) {
// Rule for express shipping
maximum(12) { "Express shipping must be under 12 hours." }
} else {
// Rule for standard shipping
minimum(24) { "Standard shipping must be at least 24 hours." }
}
}
// Custom constraint example
Order::type {
constrain("Order type must be one of 'STANDARD' or 'EXPRESS'") {
it == "STANDARD" || it == "EXPRESS"
}
}
}
3. Customizing Error Messages with Hints
You can customize the error message for any constraint using hint. You can even use the {value} placeholder to include the invalid input directly in the message.
UserProfile::fullName {
minLength(2) hint "The name '{value}' is too short. Please use at least 2 characters."
}
Crossfield checks
It is even possible to provide crossfield checks to validate against a group of fields having e.g. a specific value
data class Foo(val a: Int, val b: Int, val c: Int)
Validation<Foo> {
constrain("sum must be greater than zero") {
(a + b + c) > 0
}
}
You can also define a method on your data class and validate that:
data class Foo(val a: Int, val b: Int, val c: Int) {
val sum: Int get() = a + b + c
}
Validation<Foo> {
Foo::sum {
exclusiveMinimum(0) hint "Sum must be greater than zero"
}
}
🔗 Multiplatform & Zero Dependencies
Konform’s lean design means it has zero external dependencies and supports Kotlin Multiplatform (KMP) targets like JVM, JS, and Native. This makes it an ideal choice for building shared business logic that runs everywhere, aligning perfectly with modern Kotlin development principles.
If you’re building a REST API with Ktor, an Android app with Compose Multiplatform, or a full-stack application, Konform provides a unified, powerful, and clean validation layer.
Links
This video provides real-world context on why a team chose Konform over traditional Spring validation in their Kotlin REST applications: Validation in Kotlin using Konform By Andrew Liles & Szaby Gyurko.