
Runtime annotation-based validation of object properties with recursive traversal, identity-cycle detection, standard English error messages, numeric/string constraints, and easy custom-annotation validator registration.
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" }commonMain; runtime reflection is provided via kotlin-reflect on JVM/Android.Iterable / List / Set / Array and values in Map.ValidationResult is a sealed class with nested ValidationResult.Valid and ValidationResult.Invalid.ValidationResult.Invalid.@Min(value = "...", inclusive = true/false)@Max(value = "...", inclusive = true/false)@Range(min = "...", max = "...", minInclusive = true/false, maxInclusive = true/false)Byte, Short, Int, Long, Float, Double, and Java's BigInteger and BigDecimal.@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)@Valid – Explicit nested validation
autoInspectNestedObjects is set to false.user.address.street, items[0].quantity).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 checkA 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" }commonMain; runtime reflection is provided via kotlin-reflect on JVM/Android.Iterable / List / Set / Array and values in Map.ValidationResult is a sealed class with nested ValidationResult.Valid and ValidationResult.Invalid.ValidationResult.Invalid.@Min(value = "...", inclusive = true/false)@Max(value = "...", inclusive = true/false)@Range(min = "...", max = "...", minInclusive = true/false, maxInclusive = true/false)Byte, Short, Int, Long, Float, Double, and Java's BigInteger and BigDecimal.@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)@Valid – Explicit nested validation
autoInspectNestedObjects is set to false.user.address.street, items[0].quantity).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