Validator Pattern

Programming

Needing to validate that the properties of some data match your expectations is a common problem, whether that’s validating that the invariants of some domain construct are respected, or that the format of a request received from a client is as you expect. Some languages allow you to express rules like this as part of the type system, but for languages that don’t, we can do better than the usual chain of if/else if statements.

The example below assumes you’re using Kotlin on the Spring framework, and want to take advantage of request-context exception handling. You may not wish to throw an exception immediately when a rule fails, but instead e.g. return a false value if any rule is violated, and make a decision based on that.

Elements

First, we need to define a Rule, to express a reusable constraint:

interface Rule {
  fun isInvalid(): Boolean
  fun fail()
}

Next, a Validator to manage and execute a list of Rules. For readers unfamiliar with Kotlin, apply is a scope function allowing us to execute the lambda and return this in one step.

class Validator(private val rules: MutableList<Rule> = mutableListOf()) {

  fun validate() = rules.firstOrNull { it.isInvalid() }?.fail()

  fun specify(rule: Rule) = this.apply { rules.add(rule) }
}

Now, we can define a few Rules, e.g.:

class ShouldInclude(private val value: Any?, private val description: String) : Rule {
  override fun isInvalid() = value == null
  override fun fail() {
    throw InvalidPayloadException("$description field is missing from the request.")
  }
}

class ShouldHaveStrongPassword(private val password: String) : Rule {
  override fun isInvalid(): Boolean = password.length < 16
  override fun fail() = throw WeakPasswordException()
}

class ShouldHaveUniqueUsername(
  private val username: String,
  private val userService: UserService
) : Rule {
  override fun isInvalid(): Boolean = userService.getByUsername(username) != null
  override fun fail() = throw DuplicateUsernameException()
}

Example

So if we now wanted to validate that a request to register a new user was valid, we could use them like so:

@PostMapping
fun register(@RequestBody body: RegistrationRequest): ResponseEntity<Any> {

  Validator()
    .specify(ShouldInclude(body.firstName, "First name"))
    .specify(ShouldInclude(body.surname, "Surname"))
    .specify(ShouldInclude(body.email, "Email address"))
    .specify(ShouldInclude(body.password, "Password"))
    .specify(ShouldHaveStrongPassword(body.password!!))
    .specify(ShouldHaveUniqueUsername(body.email!!, userService))
    .validate()

  userService.register(...)

  return ResponseEntity(HttpStatus.CREATED)
}

Rules with a general scope like ShouldInclude or e.g. ShouldBeValidUserId can be reused in multiple places, and since we throw the relevant exception (or return false, if you prefer) the moment we encounter a failing rule, we can order them so that inputs to later rules can be assumed to be valid according to earlier constraints. In the example controller method above, we can cast body.email and body.password to non-nullable Strings with !! because we’ve already checked them with ShouldInclude.

Going Further

Naming constraints using Rule types allows you to represent the validation logic in a more declarative style. If you had a need for it you could create special combination rules like AndRule or OrRule that compose their constraints from other rules, and treat them specially in the Validator, so as to say something like:

Validator()
  .specifyEither(
    ShouldBeValidUserId(body.userId),
    ShouldBeValidEmailAddress(body.email)
  )
  ...
  .validate()