DSL Validations: Properties

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., Maps or Sets, 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
}