Note: This is post 1 of (an expected) 4 part series. Links to future posts will be added here when published.
Whether you aware of it or not, you likely leverage Jakarta Bean Validations to validate class properties or method parameters: @NotBlank
, @NotNull
and @Email
are annotations that inject validation. Furthermore, complete objects and their sub-objects are validated when @Valid
is used, e.g., a method parameter void method(@Valid object: Class)
.
Annotation-based validations on class properties are simple: the validation either passes or fails for that specific property, full stop. More nuanced, complex validations where multiple properties are validated as a unit requires implementing a custom class validator, which, in my experiences, often devolves into tangled spaghetti code. There must be a better way!
An alternative approach is to create a domain-specific language for class-level validations that is simple to implement, understandable, and extendable, and follows the validation specification.
Applying The Specification
The specification defines the Validator
interface that custom validators implement. The interface defines six possible methods, but the most common (useful) one is:
<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);
This series of posts will show how to create custom validators for classes using a declarative rather than a coding approach . This post demonstrates implementing individual property validators which are the initial building blocks for the DSL solution.
Implementing Property Validators
Creating a validator for a single property requires extending from a base class and implementing/overriding the method that does the actual validation. Though not required here, the validate
method above will be used.
AbstractPropertyValidator
AbstractPropertyValidator
is the parent/base class for each validator that implements equals
and hashCode
for all concrete implementations, useful when validators are held in a collection (e.g., Map
s or Set
s, will become more obvious in future posts).
The class has two properties/members provided by the child class during construction:
propertyName
is informational only, used when creating a violation when validation fails;getter
is the getter function for the specific property is validated from an object. The generic<S>
is the validated object’s class and<T>
is the return data type for the getter.
abstract class AbstractPropertyValidator<T,S> (
protected val propertyName: String,
protected val getter: S.() -> T?) : AbstractValidator<S>() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AbstractPropertyValidator<*, *>
return Objects.equals(propertyName, other.propertyName)
&& Objects.equals(getter, other.getter);
}
override fun hashCode(): Int {
return Objects.hash(propertyName, getter)
}
}
NOTE: This abstract class extends AbstractValidator
which is defined later in this post.
Implementing A Validator
For this example, we need to validate a property’s value is either negative and divisible by 3 or positive and divisible by 7; any other values, including null and zero, are invalid.
[Obviously very contrived, I’ve never had this requirement in real life, but makes for a fairly straight-forward example.]
Let’s implement LongSevensAndThreesValidator
to do the validation described. Each property validator class extends AbstractPropertyValidator
and implements validate
which
- invokes the getter method to retrieve the property value;
- validates that the value is valid or not.
The generic <S>
is the class being validated, described above in AbstractPropertyValidator
. The <T>
generic supplied to AbstractPropertyValidator
is hard-coded as Long
because concrete implementation expects a Long
.
class LongSevensAndThreesValidator<S> (propertyName: String,
getter: S.() -> Long?)
: AbstractPropertyValidator<Long, S>(propertyName, getter) {
override fun validate(source: S,
errors: MutableSet<ConstraintViolation<S>>) : Boolean {
val value = getter.invoke(source)
return if (value != null &&
((value > 0L && value % 7 == 0L) ||
(value < 0L && value % 3 == 0L))) {
true
} else {
errors.add(createViolation(
source,
ERROR_MESSAGE.format(propertyName),
ERROR_MESSAGE,
propertyName,
value))
false
}
}
companion object {
private const val ERROR_MESSAGE = "%s must either greater than zero and divisible by 7 or less than zero and divisible by 3"
}
}
The method createViolation
is called when the validation fails and is declared in AbstractValidator
(below).
CAVEAT EMPTOR: execution time increases with complexity, and validating against remote resources – services, databases, files, etc. – increases latency, perhaps dramatically, and introduces more error conditions to handle.
Putting It All Together
In this example, the class MyExampleClass
has multiple properties, one of which myValidatedProperty
is a Long
which is validated by `LongSevensAndThreesValidator. The following is example code to determine whether the property value is correct or not.
val myExampleObject = MyExampleClass(....)
// Create the property validator by specifying which property to check.
val validator = LongSevensAndThreesValidator(
"myValidatedProperty",
MyExampleClass::myValidatedProperty)
// Validate the property
val violations = mutableSetOf<ConstraintViolation<T>>()
validator.validate(myExampleObject, violations)
// empty collection means successful validation
val successfullyValidated = violations.isEmpty()
The Set<ConstraintViolation<T>>
contains details about failed validations. Though a collection is not necessary for a single property, you’ll see its usefulness in future posts.
Final Comments
This post introduces property validators as the basic building block upon which the entire DSL is built. Future posts use and extend property validators to implement complete object validations.
Part 2: DSL Validations: Child Properties
Supporting Code
PropertyOperatorValidator
The base interface that declares the method implemented by all validators. The generic <S>
is the class or interface against which a validation is done.
interface PropertyOperatorValidator<S> {
fun validate(source: S,
errors: MutableSet<ConstraintViolation<S>>): Boolean
}
AbstractValidator
The base class that implements helper methods for creating and collecting violations for failed validations. The generic <S>
is the class or interface against which a validation is done.
abstract class AbstractValidator<S> (): PropertyOperatorValidator<S> {
protected fun addViolation(source: S,
message: String,
messageTemplate: String,
property: String,
value: Any?,
errors: MutableSet<ConstraintViolation<S>>) {
errors.add(
createViolation(source,
message,
messageTemplate,
property,
value))
}
protected fun createViolation(source: S,
message: String,
messageTemplate: String,
property: String, value: Any?) : ConstraintViolation<S> {
return ConcreteConstraintViolationImpl<S>(
source,
message,
messageTemplate,
property,
value)
}
}
ConcreteConstraintViolation
All validate
methods are passed Set<ConstraintViolation>
to which information about failed validations are added, an empty collection indicating all validations succeeded. ConstraintViolation
is an interface, so ConcreteConstraintViolation
is the actual instance created when the validation fails. The generic <S>
is the class or interface against which a validation is done.
class DatasiteConstraintViolationImpl<S> (
private val rootBean: S,
private val message: String,
private val messageTemplate: String,
private val propertyPath: Path,
private val invalidValue: Any?): ConstraintViolation<S> {
constructor(
rootBean: S,
message: String,
messageTemplate: String,
propertyName: String,
invalidValue: Any?
)
: this(
rootBean,
message,
messageTemplate,
PathImpl.createPathFromString(propertyName),
invalidValue)
// Define the getters for the ConstraintViolation interface
}