-
Notifications
You must be signed in to change notification settings - Fork 1
ValidationScope
validateCollecting runs all rules unconditionally and returns a ValidationResult containing every violation that was
produced. A failed rule never prevents subsequent rules from running.
val result = validateCollecting {
verify(user::name).notBlank().minLength(3)
verify(user::email).notBlank()
verify(user::age).atLeast(18)
}ValidationResult exposes two properties:
result.isValid // true if no violations were produced
result.isInvalid // true if at least one violation was producedAnd four ways to act on it:
// Run a block if valid
result.onValid {
println("All good!")
}
// Run a block with the violation list if invalid
result.onInvalid { violations ->
violations.forEach { println(it.reason) }
}
// Throw a ValidationException if invalid, do nothing if valid
result.throwOnInvalid()
// Branch on both outcomes and return a value
val response = result.fold(
onValid = { Response.ok() },
onInvalid = { violations -> Response.badRequest(violations) }
)Use validateCollecting when you want to report all problems at once — forms, API request validation, DTO validation.
validateThrowing stops at the first failed rule and throws immediately.
validateThrowing {
verify(order::total).atLeast(minimumAmount)
verify(order::items).minSize(1)
}A failed rule throws a ViolationException, which carries the single violation that caused it. ViolationException is
a subtype of ValidationException, which carries a list — the subtype relationship matters when catching exceptions
from code that may use either scope.
try {
validateThrowing {
verify(order::total).atLeast(minimumAmount)
verify(order::items).minSize(1)
}
} catch (e: ViolationException) {
println(e.violation.reason)
}If all rules pass, validateThrowing returns the value of the block normally — so it can be used inline:
val validated = validateThrowing {
verify(user::age).atLeast(18)
processUser(user)
}Use validateThrowing when a single violation means the operation cannot proceed — business logic, use cases, internal
invariants.
verify is the most common way to apply rules, but the scope exposes two lower-level functions that are useful when you
need more control.
enforce takes a Rule — a functional interface whose single method returns a Violation on failure, or null on
success. It can be passed as a lambda:
validateCollecting {
enforce {
if (user.name.isBlank()) violation("Name must not be blank") else null
}
}failIf is a convenience wrapper that reads more naturally for simple boolean checks:
validateCollecting {
failIf({ user.age < 18 }) {
violation("User must be at least 18 years old")
}
}Both behave according to the active scope — in a collecting scope they accumulate violations, in a throwing scope they throw immediately.
ValidationScope is an interface with two members: validationContext and enforce. Implementing it is
straightforward when the built-in collecting and throwing behaviors don't fit your needs — for example, a scope that
logs violations as they occur:
class LoggingValidationScope(
override val validationContext: ValidationContext,
private val logger: Logger,
) : ValidationScope {
override fun enforce(rule: Rule) {
val violation = rule.check() ?: return
logger.warn("Validation failed: ${violation.reason}")
}
}Custom scopes integrate with the rest of the library without any additional wiring — verify, failIf, and scope
extension functions all work on any ValidationScope implementation.
pathName and pathIndex let you append named or indexed segments to the validation path manually. They are covered in
depth on the ValidationContext & ValidationPath page.
- Built-in Rules — full reference for the rules available out of the box