DSL Validations: Child Properties

Scott Sosna
3 min readApr 3, 2024

--

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

Part 1 introduced the concept of property validators, providing the building blocks for DSL validations: access an object’s property and check its value.

However, property validators are limited to simple data types. Specifically, how do you validate a property on an object contained by the base object? That’s the purpose of ChildPropertyValidator validators.

ChildPropertyValidator

The ChildPropertyValidator is a special-case PropertyValidator which accesses a property which itself is an object – contained within the base object – and applies a PropertyValidator on its property.

  • propertyName is informational only, used when creating a violation when validation fails;
  • getter is the function that returns the object property. As with a generic property validator, the generic <S> defines the class on which the getter is called and <T> identifies the return data type of the getter, the class of the contained object;
  • child is the property validator for a property on the contained object.

When the property of the contained object is not null, the property validator provided is executed against that contained object; when the contained object is null, validation fails and a ConstraintViolation is created.

class ChildPropertyValidator<S,T> (propertyName: String,
getter: S.() -> T?,
val child: PropertyValidator<T>)
: AbstractPropertyValidator<T, S>(propertyName, getter) {

override fun validate(source: S,
errors: MutableSet<ConstraintViolation<S>>)
: Boolean {

// Attempt to get the subdocument
val childSource = getter.invoke(source)

// If subdocument is not-null validate child document; otherwise
// generate error and return
return if (childSource != null) {
validateChild(source, childSource, errors)
} else {
errors.add(
createViolation(source,
ERROR_MESSAGE.format(propertyName),
ERROR_MESSAGE,
propertyName,
null))
false
}
}

private fun validateChild (source: S,
childSource: T,
errors: MutableSet<ConstraintViolation<S>>)
: Boolean {

val set = mutableSetOf<ConstraintViolation<T>>()
val success = child.validate(childSource, set)

// Validator interface limits errors to single type, therefore need to recast the error as the root type rather
// than the child type/source on which we were validated. Stinks, but ConstraintViolation<*> cause other problems
if (!success) {
val error = set.first()
errors.add(
createViolation(
source,
error.message,
error.messageTemplate,
propertyName,
error.invalidValue))
}

return success
}

companion object {
private const val ERROR_MESSAGE = "%s is required for evaluating."
}
}

Putting It All Together

Let’s define a simple Kotlin data class that defines a (very) basic Student:

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
)

In this example we need to validate that the student’s address has a correctly-formatted United States zip code: five digits (i.e., 12345, most common) or five digits/hyphen/four digits (i.e., 12345–6789, Zip+4). The ZipCodeFormatValidator is the property validator that checks for either of these two formats.

The sample code demonstrates how the ZipCodeFormatValidator is wrapped by a ChildPropertyValidator to validate the zip code within the contained Address object.

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

// Create instance of property validator
val zipValidator = ZipCodeFormatValidator("address",
Address::zipCode)
// Create child property validator for the Student
val childValidator = ChildPropertyValidator("address.zipCode",
Student::address,
zipValidator)
// Validate the property
val violations = mutableSetOf<ConstraintViolation<T>>()
childValidator.validate(myStudent, violations)

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

CAVEAT EMPTOR: ChildPropertyValidator is itself a PropertyValidator and therefore it’s possible to navigate multiple levels deep; however, the readability and latency likely suffers. Weigh the trade-offs of a custom class-level validation versus implementing via the DSL.

Final Comments

While seemingly benign, ChildPropertyValidators are a necessity for building DSL validations for anything but the most simple class definitions. In Part 3, we’ll demonstrate how to combine multiple validators to do more complex class-level validations without the need of writing code.

Originally published at https://scottsosna.com/2024/03/21/dsl-validations-child-properties/

Image © 1998 Scott C. Sosna

--

--