Custom rules
A rule is anything implementing ValueValidatorRule<V> — return a ValidationResult
from validate(value). For the rules that already ship, see
built-in rules; to run an existing rule only sometimes, see
onlyWhen.
Reusable rule types
// A reusable rule: an object (or data class for parameterised rules) implementing
// ValueValidatorRule<V>. Return success for blank input — let Required own emptiness.
data object UsZipCode : ValueValidatorRule<String> {
private val zip = Regex("""^\d{5}(-\d{4})?$""")
override fun validate(value: String): ValidationResult {
if (value.isBlank()) return RegularValidationResult.success()
return if (zip.matches(value)) RegularValidationResult.success()
else RegularValidationResult.error("Enter a valid ZIP code")
}
}
Use a data class instead of data object when the rule takes parameters
(MinLength(minLength = 8) is built that way).
One-off rules (SAM lambdas)
// ValueValidatorRule is a fun interface — one-off rules can be inline SAM lambdas.
// Severity is graded: error/warning/info/success all flow through supportingText.
val NoPlusAddressing = ValueValidatorRule<String> { value ->
if ("+" in value) RegularValidationResult.warning("Plus-addressing may break receipts")
else RegularValidationResult.success()
}
Severities and results
ValidationResult.outcome() ranks ERROR > WARNING > INFO > SUCCESS. Only
ERROR blocks validate()/submission — warnings and info messages display through
supportingText without failing the field.
Two result implementations ship with the library:
RegularValidationResult.error/warning/info/success("message")— plain strings, shown verbatimResourceValidationResult— backed by ComposeStringResourcefor i18n; all the built-in rules use this
Conventions
- Return success for blank input and let
Requiredown emptiness — that way every rule composes with optional fields (listOf(UsZipCode)allows blank,listOf(Required, UsZipCode)doesn't). - Rules run on every value change; keep
validatecheap and side-effect free.