In Spring MVC a server-side validation was implemented using annotations. We could just put @Valid before the body payload in the controller method and Spring did all dirty work for us. On the other side, Spring Webflux implements a functional-style web layer, which is based on router functions and handlers. This is non-blocking API and things are different from what we know from legacy approaches. When we need to validate an input, we actually need to do it manually, as old techniques do not work. New times need new tools, and we need to build declarative validations. One of approaches can be Vavr. Another – more Webflux oriented – is Yavi library, which we will observe in this post.

An anatomy of a validator

In the previous post on validation in Vertx I used a concept of a validator. The idea is following: we abstract a validation implementation as an interface with validate() method. The purpose of this is to make the business logic independent from a particular validation library return types. For instance, as we talked last time about Vavr Validation, we know that Vavr operates monads. From the Spring Webflux perspective, we need to incorporate the validation mechanism into a reactive stream pipeline.

Therefore, we can create an interface AbstractValidator<T>, which will have one abstract method – validate(). This method returns a Mono which can contain either an instance of the supplied object or an error in case of an invalid input. Take a look on the following code snippet:

intreface AbstractValidator <T> {
    Mono<T> validate (T data);
}

Once the validate() method finds an invalid input, this pipeline will break to the error. Let take an implementation of this contract with Yavi library. This is a lambda based type safe validation for Java. It allows to build a declarative validation, instead of an annotation-based validation approach.

Build a constraint pipeline

Yavi operates validators. Each validator contains a number of constraints, that are conditions to check. There is a number of built in assertions, to name the few:

  • notNull()
  • isNull()
  • pattern()
  • email()
  • url()
  • greaterThanOrEqual() / greaterThan()
  • lessThanOrEqual() / lessThan()
  • etc.

You can note, that these are functions. There is a method chaining, so you can build constraint pipelines, that incorporate several assertion conditions.

Let have a practical example. Suppose, that we build a validator pipeline, which checks an input data for the login endpoint. We need to assert, that user supplied a valid email and a password, which is longer than 8 characters. How we can do it with Yavi? First, define a data model LoginRequest:

@Value
public class LoginRequest {
String email;
String password;
}

This is just an entity model, which we will use. Next, we can create a Validator using a builder:

Validatior<LoginRequest> validator = ValidatorBuilder.of(LoginRequest.class)
    .constraint(LoginRequest::getEmail, "email", c -> c.notNull().email())
    .constraint(LoginRequest::getPassword, "password", c -> c.notNull().greaterThanOrEqual(8))
    .build();

This is a pipeline, which we can now incorporate into our validator interface.

Implement a validator

In this section we will implement a concrete validator class LoginRequestValidator using the mentioned AbstractValidator interface. It will use the validator pipeline from the previous section as a validation mechanism:

public class LoginRequestValidator implements AbstractValidator<LoginRequest> {
    private final Validator<LoginRequest> validator;
    public LoginRequestValidator() {
        this.validator = ValidatorBuilder.of(LoginRequest.class)
        .constraint(LoginRequest::getEmail, "email", c -> c.notNull().email())
        .constraint(LoginRequest::getPassword, "password", c -> c.notNull().greaterThanOrEqual(8))
        .build();
    }

    @Override
    public Mono<LoginRequest> validate(LoginRequest data) {
        if (validator.validate(data).isValid()) return Mono.just(data);
        return Mono.error(ValidationException::new);
    }
}

Note, that Yavi provides the isValid() method, which helps us to determine if the input is valid or not. In the first case we just pass the data forward into a reactive stream to the next subscriber. Otherwise, we raise an error, which can be produced as a corresponding return message, as we have seen in the post on the error handling in Spring Webflux. The last remaining thing is to put it into a handler function.

Incorporate a validator into a handler

Suppose, that we have an auth handler, which is responsible for signup/login operations. The login endpoint can look like this:

Mono<ServerResponse> login (ServerRequest request){
return request.bodyToMono(LoginRequest.class)
        .flatMap(service::login)
        .flatMap(result -> ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON).bodyValue(result))
        .onErrorResume(ExceptionMapper::map);
}

We can put our validator class into this handler method. For this we need to achieve three things:

  1. Create a LoginRequestValidator instance before the pipeline
  2. Put a validator as a first map method
  3. Tell the ExceptionMapper class, that the validation exception should be mapped to the bad request message (more in this post)

Consider the follwoing code snippet:

Mono<ServerResponse> login (ServerRequest request){
LoginRequestValidator validator = new LoginRequestValidator();
return request.bodyToMono(LoginRequest.class)
        .flatMap(validator::validate)
        .flatMap(service::login)
        .flatMap(result -> ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON).bodyValue(result))
        .onErrorResume(ExceptionMapper::map);
}

That is how we can implement a functional validation in Spring Webflux using Yavi library. If you have questions, you can ask them in comments below or contact me.