Spring WebFlux — Validation

Prateek
3 min readMay 6, 2022

In this example, we’ll see the way to perform the Bean validation using the Spring WebFlux Validation.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>reactive-validation</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>reactive-validation</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>

Employee.java

package com.example.dto;


import com.example.validation.ValidSSN;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ValidSSN
public class Employee {
private String name;
@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;

@NotNull(message = "{ssn.not.null}")
private Integer ssn;
}

MessageConfig.java

package com.example.config;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@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;
}
}

ValidationHandler.java

package com.example.exceptionhandler;

import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.support.WebExchangeBindException;

import java.util.List;
import java.util.stream.Collectors;

@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);
}
}

EmployeeService.java

package com.example.service;

import com.example.dto.Employee;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import javax.validation.constraints.AssertTrue;

@Service
public class EmployeeService {
@AssertTrue
public Mono<Employee> registerEmployee(Mono<Employee> employeeMono){
return employeeMono
.doOnNext(System.out::println);
}
}

ValidSSN.java

package com.example.validation;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@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 {};
}

ConstraintValidator.java

package com.example.validation;

import com.example.dto.Employee;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class SSNConstraintValidator implements ConstraintValidator<ValidSSN, Employee> {

@Override
public boolean isValid(Employee employee, ConstraintValidatorContext constraintValidatorContext) {
if (employee.getCountry() != null && "".equals(employee.getCountry())) {
return false;
}
return true;
}
}

EmployeeController.java

package com.example.controller;

import com.example.dto.Employee;
import com.example.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

import javax.validation.Valid;

@RestController
@RequestMapping("employee")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;

@PostMapping("save")
public Mono<Employee> register(@Valid @RequestBody Mono<Employee> employee){
return this.employeeService.registerEmployee(employee);
}
}

MainApp.java

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ReactiveValidationApplication {

public static void main(String[] args) {
SpringApplication.run(ReactiveValidationApplication.class, args);
}

}

How to test?

curl — location — request POST ‘http://localhost:8080/employee/save' \
— header ‘Content-Type: application/json’ \
— data-raw ‘{
“firstName”: “john”,
“lastName”: “doe”,
“age”: 50,
“email”: “abcd@abcd.com
}’

[
"SSN can not be empty",
"Country can not be empty"
]

— — — — — — — — — — — — — — — — — — — — — — — — — —

curl — location — request POST ‘http://localhost:8080/employee/save' \
— header ‘Content-Type: application/json’ \
— data-raw ‘{
“firstName”: “john”,
“lastName”: “doe”,
“age”: 60,
“email”: “abcd@abcd.com
}’

[
"Country can not be empty",
"SSN can not be empty",
"Age max allowed is 50"
]

--

--