Eclipse Vertx is a robust technology to build reactive microservices or serverless functions. When you develop such applications, the role of server side validation rises. In traditional web applications we combine both client-side and server-side validation techniques. However, in API world we can be in situations without clients (when you have a public API). We also want to provide a unified validation approach for all types of clients (web, mobile, desktop) without a potential risk of concrete platform.

Java traditionally use annotation-based validation, for instance in JSR 303 implementations, like Hibernate Validator. In my previous post I showed you tinyvalidator library, that also uses same pattern. For Vertx applications you may want to have something more powerful from the one side, and something, that does not violate its nature from the another side. In this article I will demonstrate you how to implement validation using Vavr in a functional style.

Want to know more about Vertx?

I develop with Eclipse Vertx as my main specialization and I built cloud systems for Mercedes Benz, Tesco and Citi Bank. Now, I want to share with you my experience and make your journey in the Vertx universe easier

Build validation API

For the purpose of this post I will take an authentication microservice, which is a part of more complex tutorial. We will do only validation for signup and login routes, however we omit concrete auth code, as it is not part of this topic.

In this section we will implement validation with Vavr.

Everything starts with a validator

The main entry point for our API is a validator class. This component does a bean assertion, e.g. validate that the supplied entity satisfies some rules. Comparing to annotation-based approach, these rules are defined not in a bean (POJO), rather, in a validator itself. That brings a better encapsulation and makes entities reusable, because they are free from annotation dependencies.

Each validator implements an AbstractValidator contract, that has one method validate(). Take a look on its definition:

public interface AbstractValidator<T> {

  Validation<ValidatonError, T> validate (T bean);

}

It returns Vavr Validation object, which is a monad implementation, that contains an error and a bean. In this case ValidationError is a simple subclass of RuntimeException, that contains an validation error message:

public class ValidatonError extends RuntimeException{

  public ValidatonError(String message){
    super(message);
  }
}

Rules helpers

As I mentioned already, validation rules are placed not in a bean, but in a validator. Such rules usually contain non null validation, length assertions, format checks (like does a field contain a valid email) and so on. For a purpose of re-usability, I placed them separately inside ValidationHelpers class.

It incorporates a number of static methods, that assert following situations:

  • Length assertion = the given input string has a length of X characters
  • Pattern assertion = the given input string satisfies the given regular expression
  • Email assertion = the special case of the previous one; asserts a string against email regex

Take a look on their implementations in a code snippet below:

class ValidationHelpers {

  static Validation<String, String> validateRequiredLength (String name, String input, int length) {
    if (input.length() < length) {
      return Validation.invalid(name.concat(": length is less than required"));
    }
    return Validation.valid(input);
  }

  static Validation<String, String> validateAgainstPattern (String name, String input, String regex) {
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(input);
    if (!matcher.matches()) {
      return Validation.invalid(name.concat(": does not match a required pattern"));
    }
    return Validation.valid(input);
  }

  static Validation<String, String> validateIsEmail (String name, String input) {
    final String regex = "^[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$";
    return validateAgainstPattern(name, input, regex);
  }
}

Note, that each method also accepts a name argument. This is useful to provide a name of a field as a part of an error message to make it more understandable for user.

Implement a validator for AuthRequest

Now we can combine these helpers into a validator for an authentication request object. We need to check following constraints:

  • Email is in a valid format
  • Password has a length of 8 or more characters

Let implement the validator interface for the given situation:

@Override
public Validation<ValidatonError, AuthRequest> validate(AuthRequest request) {

String email = request.getEmail();
String password = request.getPassword();

return Validation.combine(
      ValidationHelpers.validateIsEmail("Email", email),
      ValidationHelpers.validateRequiredLength("Password", password, 8)
    ).ap((e, p) -> new AuthRequest(e, p))
    .mapError(seq -> {
        String message = seq.toStream().map(s -> s.concat(", ")).collect(Collectors.joining());
        ValidatonError error = new ValidatonError(message);
        return error;
      });
}

What can we see here?

  • Validation object allows to construct itself from several validations, in our case, they are validation helpers
  • Using ap() method we construct a new bean (although, we can return the one from arguments)
  • mapError() function takes a sequence of messages (strings), join them in one string and creates a ValidationError object

In order to check, that the validator behaves as expected, we can write a simple unit test. It will asserts two situations: correct input (email, password) and bad input (does not satisfy rules). Take a look on the code snippet below:

class AuthRequestValidatorTest {

  @Test
  void validateSuccessTest() {
    AuthRequest request = new AuthRequest("john.doe@email.com", "password");
    AuthRequestValidator validator = new AuthRequestValidator();
    Validation<ValidatonError, AuthRequest> result = validator.validate(request);
    Assertions.assertTrue(result.isValid());
    Assertions.assertEquals(request.getEmail(), result.get().getEmail());
    Assertions.assertEquals(request.getPassword(), result.get().getPassword());
  }

  @Test
  void validateFailedTest() {
    AuthRequest request = new AuthRequest("john.doe", "secret");
    AuthRequestValidator validator = new AuthRequestValidator();
    Validation<ValidatonError, AuthRequest> result = validator.validate(request);
    Assertions.assertTrue(result.isInvalid());
    Assertions.assertNotNull(result.getError().getMessage());
  }
}

Now we can move to the Vertx layer and use this validator to check HTTP request’s body.

Go Vertx

In this subsection we will move to Vertx layer and implement a signup handler with the AuthRequestValidator. We will not going into each line of a verticle code, as it is not a subject of this post. Also, you can find a complete source in this github repository. Contrary, we will concentrate on signupHandler method.

Check the code snippet, presented below:

private void signupHandler (RoutingContext context) {
    JsonObject body = context.getBodyAsJson();
    AuthRequest request = Json.decodeValue(body.toBuffer(), AuthRequest.class);

    AuthRequestValidator validator = new AuthRequestValidator();

    Validation<ValidatonError, AuthRequest> validation = validator.validate(request);

    if (validation.isValid()) {
      JsonObject payload = new JsonObject().put("message", "User was created");
      context.response().setStatusCode(200).end(payload.encodePrettily());
    } else {
      String message = validation.getError().getMessage();
      JsonObject payload = new JsonObject().put("reason", message);
      context.response().setStatusCode(400).end(payload.encodePrettily());
    }
}

Note, that actual signup logic is omitted, because we focus only on validation here. You can see, that we basically complete following steps:

  1. Get body payload
  2. Create AuthRequestValidator instance
  3. Do validation
  4. If result is valid, we return User was created message
  5. Otherwise, we return bad request response

We can run our service and verify, that everything works as expected. The first situation is a auth request with invalid email and password:

Graph 1. Signup with bad payload

The second case is a valid email address, but with a short password (less than 8 chars). As you see, the error message is another:

Graph 2. Shortpassword

And finally, let call the signup endpoint with a valid payload:

Graph 3. Valid payload

As it is demonstrated in this post, building a validation for Vertx is not a rocket science. You can easily avoid annotation-based solutions, like Hibernate Validator and implement more functional code with Vavr. You can take a complete source code here. If you have questions, don’t hesitate to ask them in comments or contact me.

Want to know more about Vertx?

I develop with Eclipse Vertx as my main specialization and I built cloud systems for Mercedes Benz, Tesco and Citi Bank. Now, I want to share with you my experience and make your journey in the Vertx universe easier