A Kotlin validation library ready for Kotlin Multiplatform (KMP). Skrutiny inspects class instances at runtime, validates property values against library annotations, performs recursive validation on nested objects and collections, and allows users to register custom annotations and validators.
skrutiny = { module = "com.elevup:skrutiny:skrutiny", version.ref = "skrutiny" }- Runtime Annotation-based Validation: Reads annotations directly from properties at runtime.
- Kotlin Multiplatform Ready: Core validation interfaces, annotations, registry, and models are in
commonMain; runtime reflection is provided viakotlin-reflecton JVM/Android. - Recursive Validation:
- Automatically inspects nested complex objects and data classes.
- Automatically traverses items in
Iterable/List/Set/Arrayand values inMap. - Identity-based cycle detection avoids infinite recursion on circular object graphs.
- Sealed ValidationResult with Nested Valid / Invalid:
ValidationResultis a sealed class with nestedValidationResult.ValidandValidationResult.Invalid.- Violations and errors are contained only in
ValidationResult.Invalid.
- Built-in Constraints:
- Numbers & Decimals:
@Min(value = "...", inclusive = true/false)@Max(value = "...", inclusive = true/false)@Range(min = "...", max = "...", minInclusive = true/false, maxInclusive = true/false)- Exact precision for
Byte,Short,Int,Long,Float,Double, and Java'sBigIntegerandBigDecimal. - Configurable inclusivity and exclusivity.
- Strings:
@Length(min, max)– English error format:"Length of {field_name} is {X}, must be between {A} and {B}"@Pattern(regex)– Regular expression matching with IntelliJ IDEA language injection (@Language("RegExp")) for auto-coloring, live syntax checking, and "Check RegExp" tool.@Email– Email address validation@NotEmpty– Non-empty check (length/size > 0)@NotBlank– Non-blank check (contains non-whitespace characters)
- Hierarchy:
@Valid– Explicit nested validation- Takes effect only when
autoInspectNestedObjectsis set tofalse.
- Takes effect only when
- Numbers & Decimals:
- Consistent Default Error Messages: Clean, standardized English error messages with property paths (
user.address.street,items[0].quantity). - Extensible: Easily register custom annotations and custom validator logic via classes or Kotlin DSL.
import com.elevup.skrutiny.annotations.*
import java.math.BigDecimal
import java.math.BigInteger
data class ProductPricing(
@NotBlank
val sku: String,
@Pattern(regex = "^[A-Z]{3}-[0-9]{4}$")
val productCode: String,
@Min("0.5")
val taxRateFloat: Float,
// Exclusive lower bound (price > 1.00), inclusive upper bound (price <= 999.99)
@Range(min = "1.00", max = "999.99", minInclusive = false, maxInclusive = true)
val priceDouble: Double,
@Min("100")
val stockCount: BigInteger,
@Min("0.01", inclusive = true)
val minOrderAmount: BigDecimal
)import com.elevup.skrutiny.api.ValidationResult
import com.elevup.skrutiny.api.Validator
val validator = Validator()
val pricing = ProductPricing(
sku = "SKU-99",
productCode = "ABC-1234",
taxRateFloat = 0.2f, // Min is 0.5
priceDouble = 1.00, // Fails because minInclusive is false
stockCount = BigInteger.valueOf(50), // Min is 100
minOrderAmount = BigDecimal("0.005") // Min is 0.01
)
val result = validator.validate(pricing)
// Exhaustive pattern matching over nested sealed classes:
when (result) {
is ValidationResult.Valid -> println("Product pricing is valid!")
is ValidationResult.Invalid -> {
println("Validation failed with ${result.violations.size} violations:")
result.violations.forEach { v ->
println("- ${v.propertyPath}: ${v.message} (got: ${v.invalidValue})")
}
}
}@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class StartsWith(val prefix: String)
class StartsWithValidator : ConstraintValidator<StartsWith, CharSequence> {
override fun isValid(annotation: StartsWith, value: CharSequence?, context: ValidationContext): Boolean {
if (value == null) return true
return value.startsWith(annotation.prefix)
}
override fun formatErrorMessage(annotation: StartsWith, value: CharSequence?, context: ValidationContext): String {
return "${context.propertyName} with value '$value' must start with '${annotation.prefix}'"
}
}
val validator = Validator {
configureRegistry {
register(StartsWith::class, StartsWithValidator())
}
}./gradlew check