How do you validate a request DTO in a reactive Spring Web application?
In a regular Spring Web application, you would simply annotate the DTO with things like @Size
, @Email
and then put @Valid
beside the controller method parameter, like so:
import jakarta.validation.constraints.*;
public class UserSignupRequestDTO {
@NotBlank(message = "Email cannot be blank")
@Email(message = "Email should be valid")
private String email;
@NotBlank(message = "Password cannot be blank")
@Size(min = 8, message = "Password must be at least 8 characters long")
private String password;
}
@RestController
public class SignupController {
@PostMapping("/signup")
public ResponseEntity<?> signup(@Valid @RequestBody UserSignupRequestDTO signupRequest) {
// ...
}
}
However, it's not an option for a reactive handler:
@Configuration
public class RouterConfig {
private final TokenHandler tokenHandler;
public RouterConfig(TokenHandler tokenHandler) {
this.tokenHandler = tokenHandler;
}
@Bean
@RouterOperation(beanClass = TokenHandler.class, beanMethod = "signUp")
public RouterFunction<ServerResponse> signUpRoute() {
return RouterFunctions.route()
.POST("/signup", tokenHandler::signUp)
.build();
}
}
// TokenHandler implementation
@Override
public Mono<ServerResponse> signUp(ServerRequest request) {
return request.bodyToMono(UserSignupRequestDto.class) // this has to be validated
.map(userMapper::toUser)
.map(userService::save)
.map(tokenService::generateTokenFor)
.transform(jwt -> ServerResponse.status(HttpStatus.CREATED).body(jwt, String.class));
}
What is Spring's suggested way of validating a request body DTO in such circumstances? I would still like to involve Jakarta's validation annotations without, obviously, writing any annotation-processing code myself.
As I've said in my comment, you can inject a Validator
. The following successfully throws a ConstraintViolationException
:
// TokenHandler implementation
@Autowired
private Validator validator;
@Override
public Mono<ServerResponse> signUp(ServerRequest request) {
return request.bodyToMono(UserSignupRequestDto.class) // this has to be validated
.map(this::validate)
.map(userMapper::toUser)
.map(userService::save)
.map(tokenService::generateTokenFor)
.transform(jwt -> ServerResponse.status(HttpStatus.CREATED).body(jwt, String.class));
}
private <T> T validate(T object) {
Set<ConstraintViolation<T>> violations = validator.validate(object);
if (violations.isEmpty()) {
return object;
}
throw new ConstraintViolationException(violations);
}
Without any error handling in place that doesn't get turned into a nice error message, but that's a next step.