validk

Validk

GitHub release (latest by date) Coveralls

Validk is a validation framework for Kotlin (JVM), largely inspired by Konform. Among other things, the design aims to solve some specialised use cases like context-aware and conditional validation.

The core framework provides a typesafe DSL and has zero dependencies.

An additional module enables integration with Micronaut. For details please see Validk Micronaut docs.

Dependency

implementation "io.resoluteworks:validk:${validkVersion}"

The basics

data class Employee(
    val name: String,
    val email: String?
)

data class Organisation(
    val name: String, val
    employees: List<Employee>
)

val validation = validation<Organisation> {
    Organisation::name { minLength(5) }
    Organisation::employees each {
        Employee::name { minLength(10) }
        Employee::email ifNotNull { email() }
    }
}

val org = Organisation(
    "A", listOf(
        Employee("John", "john@test.com"),
        Employee("Hannah Johnson",  "hanna")
    )
)
val errors = validation.validate(org)
errors?.errors?.forEach { println(it) }

This would print

ValidationError(propertyPath=name, errorMessage=must be at least 5 characters)
ValidationError(propertyPath=employees[0].name, errorMessage=must be at least 10 characters)
ValidationError(propertyPath=employees[1].email, errorMessage=must be a valid email)

Validating an object returns a ValidationErrors which is null when validation succeeds. In other words, validation is successful when the response is null, or an instance of ValidationErrors when it fails.

Please check the tests for more examples and the documentation for a full list of constraints.

Eager errors

It’s often only required to return the first failure (failed constraint) message when validating a field. This is usually the case when displaying user errors in an application and when the order of the constraints implies the next one would fail: notBlank() failing implies email() will fail, but we first want to respond with “Email is required” rather than [“Email is required”, “This is not a valid email”].

For this purpose ValidationErrors provides eager* versions of its properties, including eagerErrors and eagerErrorMessages. For a full list of properties please check the ValiationErrors docs

Context-aware and conditional validation

Validk provides the ability to access the object being validated using the withValue construct.

private data class Entity(
    val entityType: String,
    val registeredOffice: String,
    val proofOfId: String
)

private enum class EntityType { COMPANY, PERSON }

validation<Entity> {
    Entity::entityType { enum<EntityType>() }
    withValue { entity ->
        when (entity.entityType) {
            "PERSON" -> Entity::proofOfId { minLength(10) }
            "COMPANY" -> Entity::registeredOffice { minLength(5) }
        }
    }
}

Alternatively, you can add validation logic based on the value of a specific property using whenIs.

val validation = validation<Entity> {
    Entity::entityType { enum<EntityType>() }
    
    Entity::entityType.whenIs("PERSON") {
        Entity::proofOfId { minLength(10) }
    }
    
    Entity::entityType.whenIs("COMPANY") {
        Entity::registeredOffice { minLength(5) }
    }
}

ValidObject

ValidObject provides a basic mechanism for storing the validation logic within the object itself.

data class MyObject(val name: String, val age: Int) : ValidObject<MyObject> {
    override fun validation(): Validation<MyObject> {
        return validation {
            MyObject::name { notBlank() }
            MyObject::age { min(18) }
        }
    }
}

val result = MyObject("John Smith", 12).validate()

Custom messages

validation<Person> {
    Person::name {
        notBlank() message "A person needs a name"
        matches("[a-zA-Z\\s]+") message "Letters only please"
    }
}

Micronaut integration

For a complete guide on integrating Validk with Micronaut please see the reference documentation.