DSL Validations: The Whole Enchilada

Scott Sosna
4 min readApr 3, 2024

--

Note: This is part 4 of a four-part tutorial.

After introducing the concept of property validators and operators, we can tie it all together by validating complete beans in a more reusable way.

PropertyBeanValidator

PropertyBeanValidator is the worker class which evaluates a collection of PropertyValidators against a target object. The specific property validators are provided during construction and are AND’ed together as if wrapped by anAndOperator (e.g., all property validators must pass for the entire bean to validate successfully).

open class PropertyBeanValidator<T> (
validators: Set<PropertyValidator<T>>) : DefaultBeanValidator() {

override fun <T> validate(
source: T,
vararg groups: Class<*>?): Set<ConstraintViolation<T>> {
// Place to catch all the constraint violations that
// occurred during this validation
val violations = mutableSetOf<ConstraintViolation<T>>()

// Call each individual validator to determine whether
// or not the bean validates correctly
validators
.parallelStream()
.forEach {
it as PropertyValidator<T>; it.validate(source, violations)
}

return violations
}
}

Putting It All Together

We’ll reuse the Student class defined in Part 2 DSL Validations: Child Properties.

data class Address(
val line1: String?,
val line2: String?
val city: String,
val state: String,
val zipCode: String
)

data class Student(
val studentId: String,
val firstName: String?,
val lastName: String?,
val emailAddress: String?,
val localAddress: Address
)

For this example, we have three business rules to apply against a Student object:

  • firstName and lastName must both be present or missing;
  • address.line2 presence requires that address.line1 is also present;
  • address.zipCode must be formatted correctly.

Ad-Hoc Bean Validator

Bean validators created by instantiating PropertyBeanValidator directly by a factory allows callers to be provided an appropriate validator without actually knowing what needs to be validated. The factory determines the specific validations required – based on caller, data state, feature flags, etc. – and builds the validator on the fly.

val validators = setOf(
OrOperator(
"studentName",
listOf(
AndOperator(
"namePresent",
listOf(
NotBlankValidator("firstName", Student::firstName),
NotBlankValidator("lastName", Student::lastName)
)
),
AndOperator(
"nameNotPresent",
listOf(
NullOrBlankValidator("firstName", Student::firstName),
NullOrBlankValidator("lastName", Student::lastName)
),
)
"first/last name must both be present or null"
),

OrOperator(
"Line2RequiresLine1",
listOf(
ChildPropertyValidator(
"line1NotNull",
Student::localAddress,
NotBlankValidator("line1", Address::line1)),
ChildPropertyValidator(
"line2Null",
Student::localAddress,
NullOrBlankValidator("line2", Address::line2)),
),
"line2 requires line1."
),

ChildPropertyValidator(
"address.zipCode",
Student::localAddress,
ZipCodeFormatValidator("address", Address::zipCode)
)
)

val validator = PropertyBeanValidator(validators)

Class-Specific Bean Validator

A class-specific validator is useful when there is one and only one way to validate a class and consistent and correct usage across the code base. Here we extend PropertyBeanValidator and pass in the validators via an alternative constructor.

class StudentBeanValidator (validators: Set<PropertyValidator<Student>>)
: PropertyBeanValidator<Student> (validators) {

constructor() : this(getValidators())

companion object {
fun getValidators() : Set<PropertyValidator<Student>> {
return setOf(
.
.
<same validations as above>
.
.
)
}
}
}

NOTE: A little more awkward in Kotlin, as you can’t access data in the companion object before the object is constructed, but calling a method is allowed. Statics in Java would allow creating an immutable set that could be used for any number of instantiations.

Validating

// Assume the student is created from a database entry
val myStudent = retrieveStudent("studentId")

// Validate the object
val violations = mutableSetOf<ConstraintViolation<T>>()
validator.validate(myStudent, violations)

// empty collection means successful validation
val successfullyValidated = violations.isEmpty()

Annotation-Based Validation

Jakarta’s validation interface ConstraintValidator declares an annotation-driven validation which, in turn, can be defined via the DSL.

First, implement the annotation that can be applied for validating students, in this example limited to method parameters.

@Constraint(validatedBy = [StudentValidator::class])
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class ValidStudent(
val message: String = "Invalid Student record",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<out Payload>> = []
)

Next, implement the class extending ConstraintValidator that does the actual validation, using the StudentBeanValidation implemented earlier.

class StudentValidator : ConstraintValidator<ValidStudent, Student> {
override fun isValid(student: Student,
context: ConstraintValidatorContext): Boolean {
val errors = StudentBeanValidator().validate(student)
return if (errors.isNotEmpty()) {
context.disableDefaultConstraintViolation()
context.buildConstraintViolationWithTemplate(
"Student validation failed with following errors :$errors")
.addConstraintViolation()
false
} else {
true
}
}
}
}

The annotation in action:

fun registerStudentForClass(@ValidStudent student: Student): Student {
.
.
<do some work>
.
.
}

For those interested, this Baeldung tutorial dives deeper into validations than what I’ve covered.

Final Thoughts

DSL Validations is a language-independent way of checking for bean/object validity without writing ever more if-then-else statements that are uncommented, unclear and unreadable, and are easy to extend and customize for whatever specific requirements your organization has.

Supporting Code

DefaultBeanValidator

open class DefaultBeanValidator : Validator {
override fun <T> validate(source: T,
vararg groups: Class<*>?)
: Set<ConstraintViolation<T>> {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}

override fun <T> validateProperty(source: T,
propertyName: String?,
vararg groups: Class<*>?)
: Set<ConstraintViolation<T>> {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}

override fun <T> validateValue(beanType: Class<T>?,
propertyName: String?,
value: Any?, vararg groups: Class<*>?)
: Set<ConstraintViolation<T>> {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}

override fun getConstraintsForClass(clazz: Class<*>?): BeanDescriptor {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}

override fun <T : Any?> unwrap(type: Class<T>?): T {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}

override fun forExecutables(): ExecutableValidator {
throw UnsupportedOperationException (EXCEPTION_MESSAGE)
}

companion object {
const val EXCEPTION_MESSAGE = "Not yet implemented"
}
}

--

--