Overview:
In this tutorial, I would like to show you Spring WebFlux Validation for validating the beans / request inputs.
Spring WebFlux Validation:
When we expose our APIs using WebFlux, we might have a set of constraints before accepting any request for processing. For ex: Field should not be null, age should be within the given range etc. Otherwise it could be considered as a bad request!
As part of Java EE 8 standard, we have a set of annotations which could be used to validate the request object. Lets see how it works with Spring WebFlux.
Sample Application:
To demo this, Lets consider a simple application which accepts user information. We have below fields and constraints. If the constraints are met, we would register the user and respond with 200 success code. Otherwise we would be return 400 bad request with error message.
Field | Description |
---|---|
firstName | can be null |
lastName | can NOT be null |
age | min = 10 max = 50 |
email address should NOT contain any numbers. Pattern is ([a-z])+ @ ([a-z]) .com |
Project Setup:
Lets setup our Spring WebFlux project with below dependencies.
User DTO:
Lets create the UserDTO class for user registration process. We add the constraints & corresponding error messages as shown here. We can also modify to retrieve the message from property files instead of hard coding. We will discuss that later.
@Data
@ToString
public class UserDto {
private String firstName;
@NotNull(message = "Last name can not be empty")
private String lastName;
@Min(value = 10, message = "Required min age is 10")
@Max(value = 50, message = "Required max age is 50")
private int age;
@Pattern(regexp = "([a-z])+@([a-z])+\\.com", message = "Email should match the pattern a-z @ a-z .com")
private String email;
}
Service:
This is the service class which simply prints the user details on the console / save it to the DB if it is valid.
@Service
public class UserService {
@AssertTrue
public Mono<UserDto> registerUser(Mono<UserDto> userDtoMono){
return userDtoMono
.doOnNext(System.out::println);
}
}
Controller:
We expose a POST API for user registration. Make a note of the @Valid annotation which ensures that given input is validated before invoking the service method.
@RestController
@RequestMapping("user")
public class RegistrationController {
@Autowired
private UserService userService;
@PostMapping("register")
public Mono<UserDto> register(@Valid @RequestBody Mono<UserDto> userDtoMono){
return this.userService.registerUser(userDtoMono);
}
}
Spring WebFlux Validation – Demo 1:
- Now if we send a valid request like this, we get the success 200 response code.
{
"firstName": "vins",
"lastName": "vins",
"age": 30,
"email": "abcd@abcd.com"
}
- If we send the request as shown here which does not satisfy our requirement, then we get 400 bad request response code without any meaningful information.
{
"firstName": "vins",
"lastName": "vins",
"age": 60,
"email": "abcd@abcd.com"
}
@ControllerAdvice:
To respond with appropriate error messages, Lets use controller advice as shown below. Here we catch the WebExchangeBindException, find the all the errors for which bean validation failed and retrieve the error messages.
@ControllerAdvice
public class ValidationHandler {
@ExceptionHandler(WebExchangeBindException.class)
public ResponseEntity<List<String>> handleException(WebExchangeBindException e) {
var errors = e.getBindingResult()
.getAllErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
return ResponseEntity.badRequest().body(errors);
}
}
It gives the appropriate error messages as shown below.
Messages.Properties:
Instead of hard coding the messages in the class level, we might want to retrieve the messages from a property file. Lets update our UserDTO as shown here.
@Data
@ToString
public class UserDto {
private String firstName;
@NotNull(message = "{lastname.not.null}")
private String lastName;
@Min(value = 10, message = "{age.min.requirement}")
@Max(value = 50, message = "{age.max.requirement}")
private int age;
@Pattern(regexp = "([a-z])+@([a-z])+\\.com", message = "{email.pattern.mismatch}")
private String email;
}
The corresponding messages for those keys are present in a property file name messages.properties placed under src/main/resources.
age.min.requirement=Age min requirement is 10
age.max.requirement=Age max requirement is 50
lastname.not.null=Last name can not be empty
email.pattern.mismatch=Email should match pattern abcd@abcd.com
This setup requires few additional bean configurations.
@Configuration
public class MessageConfig {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
@Bean
public LocalValidatorFactoryBean validatorFactoryBean(MessageSource messageSource) {
LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
localValidatorFactoryBean.setValidationMessageSource(messageSource);
return localValidatorFactoryBean;
}
}
Now Spring WebFlux validation works just fine by reading the messages from the property files.
Conditional Validation:
There could be cases in which we might want to validate based on conditions. For ex: If the user is from US, then SSN is mandatory. For other countries, It is not mandatory!
Field | Description |
---|---|
firstName | can be null |
lastName | can NOT be null |
age | min = 10 max = 50 |
email address should NOT contain any numbers. Pattern is ([a-z])+ @ ([a-z]) .com | |
country | can NOT be null |
ssn | mandatory only if the country is US |
- To do this – lets first create an annotation.
@Documented
@Target(TYPE)
@Retention(RUNTIME)
@Constraint(validatedBy = SSNConstraintValidator.class)
public @interface ValidSSN {
String message() default "{ssn.not.null}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- Our UserDTO is updated as shown here. Make a note of the @ValidSSN on the UserDTO class.
@Data
@ToString
@ValidSSN
public class UserDto {
private String firstName;
@NotNull(message = "{lastname.not.null}")
private String lastName;
@Min(value = 10, message = "{age.min.requirement}")
@Max(value = 50, message = "{age.max.requirement}")
private int age;
@Pattern(regexp = "([a-z])+@([a-z])+\\.com", message = "{email.pattern.mismatch}")
private String email;
@NotNull(message = "{country.not.null}")
private String country;
private Integer ssn;
}
- SSN Constraint validator which is custom conditional constraint validaiton.
public class SSNConstraintValidator implements ConstraintValidator<ValidSSN, UserDto> {
@Override
public boolean isValid(UserDto userDto, ConstraintValidatorContext constraintValidatorContext) {
if("US".equals(userDto.getCountry()))
return Objects.nonNull(userDto.getSsn());
return true;
}
}
- messages properties.
age.min.requirement=Age min requirement is 10
age.max.requirement=Age max requirement is 50
lastname.not.null=Last name can not be empty
email.pattern.mismatch=Email should match pattern abcd@abcd.com
country.not.null=Country can not be empty
ssn.not.null=SSN can not be empty
Spring WebFlux Validation – Demo 2:
- I send the below request w/o SSN for the user whose country is India.
- If I change the country to US, then I get the error saying that SSN can not be empty.
Summary:
We were able to successfully demonstrate Spring WebFlux Validation and conditional validation as well.
The source code is available here.
Read more on Spring WebFlux.
Happy coding 🙂