Spring 5 Webflux functional endpoints - How to perform input validation?
Asked Answered
S

4

14

According to the current doc (5.0.0.RELEASE) Spring Webflux supports validation when working with annotated controllers:

By default if Bean Validation is present on the classpath — e.g. Hibernate Validator, the LocalValidatorFactoryBean is registered as a global Validator for use with @Valid and Validated on @Controller method arguments.

However nothing is said about how to automate it with functional endpoints. In fact, the only example of input processing in the documentation doesn't validate anything:

public Mono<ServerResponse> createPerson(ServerRequest request) { 
    Mono<Person> person = request.bodyToMono(Person.class);
    return ServerResponse.ok().build(repository.savePerson(person));
}

Are we supposed to do this manually or there is some automatic way to do it?

Sanchez answered 1/10, 2017 at 2:9 Comment(0)
S
23

In Spring version 5.0, there is no automatic way to do validation in functional endpoints, and as such validation must be done manually.

Though there are currently no concrete plans to do so, we might add some sort of validation in the future. But even then it will be an explicit method call, and not an automatic mechanism. Overall, the functional endpoint model is designed to be a lot more explicit than the annotation-based model.

Sclerosed answered 3/10, 2017 at 8:18 Comment(2)
Thanks for the clarification Arjen.Sanchez
Could you please make an example of manual validation with RouterFunctionsKiehl
N
7

As arjen-poutsma said, it seems there is no way of running automated validations on Spring 5 functional endpoints.

Spring documentation is not very clear about this, and it doesn't suggest any approach.

On this Baeldung article, you'll find an idea on how you can run validations using this approach (disclaimer: I'm the writer of the article :) )

In a nutshell, you can follow these steps:

  1. Implement Spring Validators to evaluate your resources
  2. Create an abstract class with the basic procedure that any handler will follow when processing a request, leaving up to the children classes what to do when the data is valid
  3. Make your request handler classes extend this abstract class, implementing this abstract method, stating the body it will be expecting, and what validator needs to be used to validate it

EDIT:

I've been following this related Spring issue, and it seems we now count with official documentation regarding this subject: https://docs.spring.io/spring-framework/reference/web/webflux-functional.html#webflux-fn-handler-validation

The suggested approach is to use validators as explained in the article:

Validation

A functional endpoint can use Spring’s validation facilities to apply validation to the request body.

Nike answered 4/11, 2018 at 17:7 Comment(3)
Mentioned doc has been moved here: github.com/spring-projects/spring-framework/blob/main/…Ornelas
The url seems 404 not found.Lorenz
Fixed, now using a link to the actual Spring Framework's docs instead of to Github @LorenzNike
K
0

At the current version(2.0.4.RELEASE) there isn't a way to do automatic validation with handles, however you always could make a manual validation like this:

@Slf4j
@Component
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@RequiredArgsConstructor
public class MyHandlerValidator implements HandlerValidator<MyResource> {

    Validator validator;

    @Override
    public void callValidator(final MyResource fdr) {
        final DataBinder binder = new DataBinder(fdr);
        binder.setValidator(validator);
        binder.validate();

        if (binder.getBindingResult().hasErrors()) {
            final String reason = binder.getBindingResult().getFieldError().toString();
            log.error(reason);
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, reason);
        }
    }   
}

The thing with this, its that the you should throw a WebExchangeBindException like automatic validation does, however i could't create a MethodParameter witch is a dependency to create this exception.

UPDATE: Spring show us a way to do it, which is similar to my solution, but, not enough in my opinion on documentation

Kiehl answered 10/9, 2018 at 10:34 Comment(2)
Sorry, what is the HandlerValidator interface? I couldn't find Spring or Java documentation about that. Could you provide more information?Nike
its not an spring interface, its just custom interface with one method called validate(T resource),Kiehl
O
0

Just to demo some working code. If you need simple validation based on the object annotations like:

@Value
@Builder
@Jacksonized
public class SigninRequest {
    
    @NotBlank(message = "The username is mandatory")
    @Email(message = "The username should be valid Email")
    String username;
    
    @NotBlank(message = "The password is mandatory")
    String password;
}

At the handler you need just one simple additional operator doOnNext:

@Component
@RequiredArgsConstructor
public class AuthHandler {

    private final AuthService authService;
    
    private final ObjectValidator validator;

    public Mono<ServerResponse> signin(ServerRequest request) {

        return ok().body(
                request.bodyToMono(SigninRequest.class)
                .doOnNext(validator::validate) //<-- just one single line
                        .flatMap(login -> authService.authenticate(login.getUsername(), login.getPassword())),
                AuthResult.class);

    }

}

The ObjectValidator is doing actual validation and throws the runtime exception with the 4xx error in case of validation errors:

@Component
@RequiredArgsConstructor
public class ObjectValidator {

    private final Validator validator;

    public <T> T validate(T object) {
        var errors = validator.validate(object);
        if (errors.isEmpty()) {
            return object;
        } else {
            String errorDetails = errors.stream().map(er -> er.getMessage()).collect(Collectors.joining(", "));
            throw new ObjectValidationException(errorDetails);
        }
    }
}

And the exception:

@ResponseStatus(code = HttpStatus.UNPROCESSABLE_ENTITY)
public class ObjectValidationException extends RuntimeException {

    public ObjectValidationException(String errorDetails) {
        super("Please supply the valid data: " + errorDetails);
    }
}

If you properly setup global error handling you can keep you handler code clean and reuse the object validator across all your handlers.

Ornelas answered 22/11, 2022 at 0:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.