kandra-jakarta
kandra-jakarta is a small adapter module: one class,
io.kandra.jakarta.JakartaKandraValidator<T>, that implements kandra-core's KandraValidator<T>
by delegating to a real jakarta.validation.Validator — so entities annotated with standard Jakarta
Bean Validation constraints (@NotNull, @Size, @Email, ...) get validated automatically before
every save/update, without hand-writing a KandraValidator lambda per entity.
Why this exists as a separate module
jakarta.validation-api is declared compileOnly in this module's build.gradle.kts — not
implementation. That's the whole reason kandra-jakarta is its own module rather than folded into
kandra-core or kandra-ktor: Bean Validation requires an actual provider implementation on the
runtime classpath (Hibernate Validator is the common choice), and Kandra has no opinion on which one
you use or whether you want Jakarta Bean Validation at all. Keeping the adapter in its own module
means an app that doesn't use Jakarta annotations never pulls in jakarta.validation-api — a real
compile-time dependency Kandra's core modules would otherwise force on every consumer — and an app
that does use it brings its own validator implementation alongside this thin bridge.
dependencies {
implementation("ke.co.coinx.kandra:kandra-jakarta")
implementation("org.hibernate.validator:hibernate-validator:8.0.1.Final") // your own choice of provider
}JakartaKandraValidator<T>
class JakartaKandraValidator<T : Any>(
private val validator: Validator = Validation.buildDefaultValidatorFactory().validator,
) : KandraValidator<T> {
override fun validate(entity: T): List<KandraValidationError>
}Runs validator.validate(entity) and maps every constraint violation into a
KandraValidationError(field = violation.propertyPath.toString(), message = violation.message) — the
same error shape kandra-core's KandraValidationException composes its message from.
data class User(
@field:NotBlank val name: String,
@field:Email val email: String,
@field:Min(0) val age: Int,
)
val validator = JakartaKandraValidator<User>()
val errors = validator.validate(User(name = "", email = "not-an-email", age = -1))
// [KandraValidationError("name", "must not be blank"),
// KandraValidationError("email", "must be a well-formed email address"),
// KandraValidationError("age", "must be greater than or equal to 0")]Jakarta Bean Validation reads Java-style field/getter annotations. On a Kotlin data class
constructor property, the annotation has to be told to attach to the backing field —
@field:NotBlank val name: String, not bare @NotBlank val name: String — or Hibernate Validator
silently validates nothing for that property, since the annotation lands somewhere Bean Validation
never looks. This is a Kotlin/Jakarta interop detail, not a Kandra-specific behavior, but it's the
single most common reason a Jakarta-validated entity "does nothing" the first time someone wires this
module in.
KandraConfig.validateJakarta<T>()
inline fun <reified T : Any> KandraConfig.validateJakarta()
fun <T : Any> KandraConfig.validateJakarta(klass: KClass<T>)Registers a JakartaKandraValidator<T> with install(Kandra) { } in one call — equivalent to
validate<T> { JakartaKandraValidator<T>().validate(it) }, but resolved through
KandraJakartaSupport.isAvailable first:
install(Kandra) {
register(User::class)
validateJakarta<User>()
}validateJakarta<T>() checks KandraJakartaSupport.isAvailable (whether
Validation.buildDefaultValidatorFactory() succeeds) before registering anything. If no provider
(e.g. Hibernate Validator) is on the runtime classpath, it logs a WARN naming the missing dependency
and skips registration silently — plugin install still succeeds, and the entity is simply never
validated. If you add kandra-jakarta and validateJakarta<User>() but forget the actual validator
implementation dependency, nothing will tell you at startup that validation isn't running; only a bad
row making it through unvalidated will.
KandraJakartaSupport
object KandraJakartaSupport {
val isAvailable: Boolean // by lazy — computed once, cached for the process lifetime
}Checks availability by actually constructing (and immediately closing) a default validator factory —
not just a classpath/reflection check for the provider class. by lazy's default SYNCHRONIZED
mode makes this a thread-safe, compute-once check; the result never changes for the life of the
process even if the classpath could theoretically change (it can't, at runtime, in a normal JVM
deployment).
Full API reference
kandra-jakarta on Dokka → ·
JakartaKandraValidator →
Where this shows up elsewhere on this site
/modules/kandra-core— theKandraValidator<T>interface this module adapts, and the hand-written alternative./modules/kandra-ktor—validate<T> { }, the lower-level hookvalidateJakarta<T>()wraps.