Akkurate: New library for validation of your domain models | by Matthias Schenk | Sep, 2023

The next step is to create the required constraints.

I start with the constraints for the street property.

val addressValidator = Validator<Address> 
this.street
isNotEmpty() otherwise "Street must not be empty."
constrain
it.matches("[a-zA-Z\\s]+".toRegex())
otherwise "Street must contain only letters."

The value must not be empty and also it should only contain letters. For the String type, there are also built-in validators available so I can use the isNotEmpty() – function. By default an error message of “Must not be empty” is returned in the failure case. Because I want to have a more expressive message I can use otherwise() to specify a custom error message. With this, the constraint can be reused in all areas but the resulting error message always matches the context.

For the constraint that the street only should contain letters, there is no built-in constraint available but that is no problem because I always have the option to build my own ones by using the constrain() – function. This function accepts another function as a parameter that evaluates to a Boolean value. With this, it is easy to use the matches() – function of the Kotlin standard library to evaluate the input.

Adding a validation constraint inline is only one option for using custom constraints. In case it is usable for multiple model properties or across different models I also have the possibility to move it to an extension function.

private fun Validatable<String>.onlyContainsLetters(): Constraint 
return constrain
it.matches("[a-zA-Z\\s]+".toRegex())
otherwise "Property '$this.path().first()' with value '$this.unwrap()' must contain only letters."

I need to specify the type for which the constraint can be used, in the above case it is String. Inside the function body, I can copy the inline constraint. There are 2 additional changes I made:

  • I used the path() – function to reference the property name for adding to the error message.
  • I also added the input value to the error message by calling unwrap().

In the validator block, I can now directly call the function.

val addressValidator = Validator<Address> 
this.street
isNotEmpty() otherwise "Street must not be empty."
onlyContainsLetters()

The nice thing I can still override the error message by specifying an otherwise() – function block.

The streetNumber, the zipCode, and country property validation are the same straight forward so I only show the final result:

val addressValidator = Validator<Address> 
this.street
isNotEmpty() otherwise "Street must not be empty."
onlyContainsLetters()

this.streetNumber
isNotEmpty() otherwise "Street number must not be empty."
isValidStreetNumber()

this.zipCode
isBetween(10000..99999) otherwise "Zip code must be between 10000 and 99999."

this.country
hasLengthBetween(1..3) otherwise "Country must be a 1-3 letter ISO code."

As soon as the validator is finished, I can use it to validate Address objects.

val address = Address(
id = 1,
street = "Main Street",
streetNumber = "1",
city = "Berlin",
zipCode = 12345,
country = "DE"
)

val result = addressValidator(address)

The result that is returned is either of type Success (validation successful) or Failure (at least one validation failure).

when (result) 
is ValidationResult.Failure -> this.violations
is ValidationResult.Success -> this.value

In the success case, I have access to the value that is validated and in the failure case, I get the violations (of type ConstraintViolationSet).

If you have a look at the return type in case of validation failures you can already guess that the validation result not only contains a single failure but accumulates multiple ones.

In the below example, you can see that a list of all occuring validation failures is returned.

0 = ConstraintViolation@3049 ConstraintViolation(message='Street must not be empty.', path=[street])
1 = ConstraintViolation@3050 ConstraintViolation(message='Property 'street' with value '' must contain only letters.', path=[street])
2 = ConstraintViolation@3051 ConstraintViolation(message='Street number with value '1BB' must contain only digits and at most a single letter.', path=[streetNumber])
3 = ConstraintViolation@3052 ConstraintViolation(message='Zip code must be between 10000 and 99999.', path=[zipCode])
4 = ConstraintViolation@3053 ConstraintViolation(message='Country must be a 1-3 letter ISO code.', path=[country])

Until now this is the only option. It is not possible to stop the validation process after the first constraint violation. However, according to the author, this option will come as a configuration for the creation of a validator.

val customConfig = Configuration(
...
)

val validate = Validator<Address>(customConfig)
...

These are the basics for the validation of domain models that contain properties of built-in types. In the next part, I will have a look at the validation of domain models that are composed of other custom types.

Nested Domain Model

Domain models do not always consist of types like Int, LocalDate, or String but also of custom types. How is the validation working in these cases?

In the below example, I have got a User domain model that consists of built-in types (that should be validated) but also custom types like the Address and Account that already contain their own validators.

data class User(
val id: Long,
val firstName: String,
val lastName: String,
val birthDate: LocalDate,
val address: Address,
val accounts: List<Account>
)

For the Address and the Account I don’t want to duplicate the validation code when validating a User but I want to be sure that the whole input data is validated because currently, the validation process happens after the creation of the object. With this, I’m not sure if the used Address or Accounts are valid.

Luckily there is a way to reuse the already-created validator.

val userValidator = Validator<User> 
this.firstName
onlyContainsLetters() otherwise "First name must contain only letters."

this.lastName
onlyContainsLetters() otherwise "Last name must contain only letters."

this.birthDate
isBefore(LocalDate.now()) otherwise "Birth date must be in the past."

this.address.validateWith(addressValidator)
this.accounts
isNotEmpty() otherwise "There must be at minimum one account"

this.accounts.each
validateWith(accountValidator)

For each property that I validate, I can specify an already existing validator by the validateWith() – function. In case there is a collection of elements that should be validated I can use the each() – function to validate each element with a given validator.

Testing

One of the first things I have a look at when using a new library is the testability of the functionality. Adding validation to my domain models is one thing but I want also to verify that these validations are working as expected. So let’s have a look at the testing side.

A good combination for testing is JUnit5 as the platform for test execution together with Kotest for the assertions.

I’m expected to write my tests in a similar way as shown below. Creating an Address object, validating it with the created addressValidator, and finally calling any of the shouldBe() – functions on the result.

@Test
fun `validate address with invalid empty street`() {
// given
val address = Address(
id = 1,
street = "",
streetNumber = "1",
city = "Berlin",
zipCode = 12345,
country = "Germany"
)

// when
val actual = addressValidator(address)

// then
actual shouldBe ...

The validator of Akkurate is returning a result type that can be of a Success or Failure type. So I first need to make a check if the expected type is returned and after that assert either the value of the success or the violations of the failure. Until now there are no assertions available for this that make my assert section short and precise. But that is no problem…I just add 2 extension functions that unwrap the expected result or fail.

private fun <T> ValidationResult<T>.shouldBeSuccess(): T 
return when (this)
is ValidationResult.Failure -> fail("Expected success but was failure with the following violations - $this.violations")
is ValidationResult.Success -> this.value

private fun <T> ValidationResult<T>.shouldBeFailure(): ConstraintViolationSet
return when (this)
is ValidationResult.Failure -> this.violations
is ValidationResult.Success -> fail("Expected failure but was success")

With this tests can be written in a very easy way. To make the assertion for failures a little bit more like the existing Kotest assertions, I also added an infix function for asserting for a specific error message:

private infix fun ConstraintViolationSet.shouldContain(message: String) 
if (this.none it.message == message )
fail("Expected message '$message' was not found but instead found '$this.map(ConstraintViolation::message)' ")

@Test
fun `validate address with multiple failures()
// given
val address = Address(
id = 1,
street = "",
streetNumber = "1BB",
city = "Berlin",
zipCode = 999999,
country = "Germany"
)

// when
val actual = addressValidator(address)

// then
actual.shouldBeFailure() shouldContain "Country must be a 1-3 letter ISO code."

The testing of the Akkurate library needs no special effort. It is very straightforward. Testability is a very important characteristic for me when it comes to introducing new libraries.

Compatibility with Either

In the last part of today’s article, I will have a look at how the Akkurate validation functionality is compatible with the domain model validation when using Either for exception handling. I’ve written an article about this. If you want to have more details you can have a look at it.

In contrast to the creation of domain objects and validating them afterwards I will only allow the creation of valid objects without having to remember calling the validator. A typical structure of a domain model looks like below:

class Transaction private constructor(
val id: Long,
val name: String,
val origin: Account,
val target: Account,
val amount: Double
) {

companion object {
fun create(
id: Long,
name: String,
origin: Account,
target: Account,
amount: Double
): EitherNel<Failure, Transaction> = either
// Validate input

Transaction(id, name, origin, target, amount)

Let’s have a look at how this works together with the validator that I created for the Transaction.

The first thing I have to do is add the validator after the creation of the Transaction but before the create() – function returns.

In the next step, I need to create a little helper function that maps the Success and Failure type of Akkurate to an Either.Left and Either.Right. Because currently the validation process always returns all occuring constraint violations it makes sense to use EitherNel for that.

fun <T> ValidationResult<T>.toEitherNel(): EitherNel<Failure, T> =
when (this)
is ValidationResult.Success<T> -> Either.Right(this.value)
is ValidationResult.Failure ->
Either.Left(this.violations.map Failure.ValidationFailure(it.message) .toNonEmptyList())

private fun <A> Iterable<A>.toNonEmptyList(): NonEmptyList<A> =
NonEmptyList(first(), drop(1))

I can use a toNonEmptyList() extension function without dealing with an empty list because the Failure type of Akkurate always returns at minimum one violation.

The factory for the creation of Transactions is very short with this.

fun create(
id: Long,
name: String,
origin: Account,
target: Account,
amount: Double
): EitherNel<Failure, Transaction> =
transactionValidator(Transaction(id, name, origin, target, amount)).toEitherNel()

The validator can be a private property of the companion object. So that it can be used internally, is only instantiated once, and from outside of the domain model no information is available.

Testing works with this in the same ways as I’m used working with Either.

@Test
fun `create transaction fails on empty name`()
// given
val id = 1L
val name = ""

// when
val origin = Account(
id = 1L,
name = "account1",
transactions = emptyList()
)
val target = Account(
id = 2L,
name = "account2",
transactions = emptyList()
)
val amount = 100.0

// when
val result = Transaction.create(
id = id,
name = name,
origin = origin,
target = target,
amount = amount
)

// then
result.shouldBeLeft().first().shouldBeTypeOf<Failure.ValidationFailure>()

As soon as it is possible to stop the validation process after the first constraint violation, I can also write a toEither() – function that wraps the failure in a single ValidationFailure object.

fun <T> ValidationResult<T>.toEither(): Either<Failure, T> =
when (this)
is ValidationResult.Success<T> -> Either.Right(this.value)
is ValidationResult.Failure ->
Either.Left(Failure.ValidationFailure(this.violation.message)

Summary

Today I had a look at the new validation library that is available for Kotlin — Akkurate. It’s the first time I started using a new library from the very beginning. In this phase of a new library, it is very important for the developer to get valuable feedback so that the further shaping of the library can meet the requirements and use cases of the library users.

So for me, this was a good possibility to use some of my existing requirements for validating domain models and check how this can be done with Akkurate. Because the library was introduced this week this is just an interim summary.

Akkurate makes it possible to group all validation constraints in a single place independent of how the validation is done in the projects. Creating domain models (without restrictions) and passing them to the validator is possible, the same as moving the validator inside the domain model and throwing exceptions or returning a failure type (like Either.Left).

Constraints that are used across different domain models can be extracted and shared. The extraction also improves the readability inside the validator block.

A last plus point for me is the good and easy testability of the validation constraints that are written with Akkurate.

In summary, I can say that I will give the library a chance and I’m excited about which direction it is developing. Supporting open-source libraries even those not coming from well-known companies or organizations. So I just can advice you to give it a try or at minimum give feedback.

As a last hint, if my explanations in this article are not enough you can find very good official documentation: https://akkurate.dev/docs/overview.html

Next Post

Marigold unveils all-in-one relationship marketing solution

Marigold has unveiled a comprehensive relationship marketing solution, designed to achieve hyper-personalization at scale while navigating complex privacy and deliverability challenges. “Relationship Marketing is a long-term strategy focused on the customer relationship over time, not on a single touchpoint, campaign or transaction,” said Desta Price, chief product officer of Marigold, […]
Marigold unveils all-in-one relationship marketing solution

You May Like