Blog Infos
Author
Published
Topics
Published
Topics

Today I want to have a look at a new validation library that can be used in your Kotlin application to validate domain models according to your business requirements. The library I am talking about is Akkurate.

You will ask what is Akkurate? Never heard about it before. That’s right. The library was released in the first version this week, so that developer feedback can be collected in order to sharpen the functionality that is provided and maybe fulfill new use cases driven by users.

There are so many existing validation libraries available from big and well-known ones like Jakarta Bean Validation (to just name a single one) to a lot of small ones available on GitHub (a simple search returns too many results to have an overview), why then exactly Akkurate? I’m not the developer of the library, but I had the chance to take part in a survey, that the author created to get feedback about the requirements developers have for using validation libraries in their applications. I also get the chance to have a look at how the implementation is planned and especially how it can be used. I was fascinated by how concise validation constraints can be written and how easily they can be extended to match the requirements. So I‘m excited that the first version was released.

This article is about my first experience with using Akkurate for my typical use cases when it comes to the validation of domain models in my Kotlin applications.

Akkurate

Let’s start the journey by adding Akkurate to my application. Akkurate is using code generation so it is necessary to add the KSP plugin to the build.gradle.kts file.

plugins{
  ...
  id("com.google.devtools.ksp") version "1.9.10-1.0.13"
}

I will later show the code that is generated by the library.

Next, I need to add some dependencies to the corresponding section.

dependencies{
  implementation("dev.nesk.akkurate:akkurate-core:0.1.0")
  implementation("dev.nesk.akkurate:akkurate-ksp-plugin:0.1.0")
  ksp("dev.nesk.akkurate:akkurate-ksp-plugin:0.1.0")
}

With this, I am ready to start…

Plain Domain Model

I take an easy example without a nested structure to show the basic usage of the validation process.

data class Address(
    val id: Long,
    val street: String,
    val streetNumber: String
    val city: String,
    val zipCode: Int,
    val country: String
)

The address object consists of several properties (of basic built-in types) which have the following requirements:

  • The street should only contain letters, no digits.
  • The streetNumber should either contain only digits or contain a digit and a letter.
  • The zipCode must be between 10000 and 99999.
  • The country is represented as a code of 3 chars.

The validator for the Address can be created by specifying the type of object that should be validated. But this does not work out of the box. I need to use a marker annotation on the domain model so that the KSP plugin knows for which classes to create the necessary code.

@Validate
data class Address(
    val id: Long,
    val street: String,
    val streetNumber: String,
    val city: String,
    val zipCode: Int,
    val country: String
)

val addressValidator = Validator<Address>{
    // Here comes the validation
}

The code that is created for the Address contains the below content:

public val Validatable<Address>.id: Validatable<Long>
  @JvmName(name = "validatableAddressId")
  get() = validatableOf(Address::id)

public val Validatable<Address?>.id: Validatable<Long?>
  @JvmName(name = "validatableNullableAddressId")
  get() = validatableOf(Address::id as KProperty1)

public val Validatable<Address>.street: Validatable<String>
  @JvmName(name = "validatableAddressStreet")
  get() = validatableOf(Address::street)

public val Validatable<Address?>.street: Validatable<String?>
  @JvmName(name = "validatableNullableAddressStreet")
  get() = validatableOf(Address::street as KProperty1)

...

For every available property of the domain model 2 properties are created one for the non-nullable and one for the nullable type. The generation of the code is done as soon as a ./gradlew build is triggered.

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."}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Brick by Brick: Building Open Source libraries

So you have an idea, and you want to publish a library for it? But where do you start? Doing Open Source is a fine art which requires skill you don’t easily learn on schoolbooks.
Watch Video

Brick by Brick: Building Open Source libraries

Paolo Rotolo & Nicola Corti
Android Engineer & Kotlin GDE - Android
Nextome & React

Brick by Brick: Building Open Source libraries

Paolo Rotolo & Nic ...
Android Engineer & K ...
Nextome & React

Brick by Brick: Building Open Source libraries

Paolo Rotolo & N ...
Android Engineer & Kotlin ...
Nextome & React

Jobs

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 IntLocalDate, 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

You can find the code I used for this article in the Github repository: https://github.com/PoisonedYouth/kotlin-akkurate

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Modern mobile applications are already quite serious enterprise projects that are developed by hundreds…
READ MORE
blog
Ask yourself a fairly standard question for any interview to fill an Android position:…
READ MORE
blog
Kotlin Multiplatform Mobile (or simply Multiplatform Mobile) is a new SDK from JetBrains that…
READ MORE
blog
If you’ve been following my stories, this is the sequel of an article I’ve…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu