Validations in Spring
Spring integrates Bean Validation and gives you more
01. Validations in Java world
Validating data is a common task that occurs throughout an application, from the presentation layer to the persistence layer. Often the same validation logic is implemented in each layer, proving to be time-consuming and error-prone.
JSR (Java Specification Requests) has developed a Java Bean validation specification. Javax and Hibernate have implemented the specification.
Spring supports Bean validation and integrates Javax and Hibernate-validation and also adds more helper class to allow better validation.
I’ll use the following DTO as an example:
@Data
public class ToDoDto implements Serializable {
private static final long serialVersionUID = 1L;
private UUID uuid;
@Email(message = "Please provide a valid Email")
private String email;
@Range(min = 13, max = 13, message = "Please provide a valid phone number. E.g., 0064-01-234-5678")
private Integer phoneNumber;
private String postAddress;
@Digits(integer = 4, fraction =0,message = "Please provide a valid post code" )
private String postCode;
@NotBlank(message = "Content cannot be empty")
private String content;
@Future()
private LocalDateTime due;
}
Bean Validation API
Bean validation annotation constraints can be applied on types, fields, methods, constructors, parameters, container elements, and other container annotations. Validation is applied not only to the object level, but it can also be inherited from super classes. Entire object graphs can be validated, meaning that if a class declares a field that has the type of a separate class containing validation, cascading validation can occur.
It is specifically not tied to either the web tier or the persistence tier, and is available for both server-side application programming. It evolves as to 3 versions now:
- JSR-303 : Bean Validation
Hibernate Validator 4.3.1.Final
- JSR 349 : Bean Validation 1.1
Hibernate Validator 5.1.1.Final
- JSR 380 : Bean Validation 2.0 (Jakarta Bean Validation)
Hibernate Validator 6.0.17.Final
Official package
javax.validation is JavaBean Validation official package
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
Hibernate implementation
You have noticed, hibernate has implemented it and uses it in different versions.
Hibernate-Validator
extends javax.validation
, e.g., it adds @Range
and @Length
.
You can mix the use of javax.validation.constraints
and org.hibernate.validator.constraints
.
for example, for our case, we imported both of them
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Email;
import javax.validation.constraints.Future;
import javax.validation.constraints.NotEmpty;
Hibernate-Validator Unit tests
You can get a validator by a ValidatorFactory
and you can also just get it by @Autowire
because Spring already provides a default validator.
class ToDoDtoValidationTest {
//@Autowired Validator validate
private Validator validator;
@BeforeEach
void setUp() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
@Test
public void tesAllGood() {
ToDoDto dto = new ToDoDto();
dto.setEmail("example@example.com");
dto.setPostCode(1234);
dto.setContent("example");
Set<ConstraintViolation<ToDoDto>> violations = validator.validate(dto);
Assertions.assertTrue(violations.isEmpty(), "should be empty, however we got violations, size: " + violations.size());
}
@Test
public void testContentIsEmpty() {
ToDoDto dto = new ToDoDto();
dto.setEmail("example@example.com");
dto.setPostCode(1234);
Set<ConstraintViolation<ToDoDto>> violations = validator.validate(dto);
Assertions.assertEquals(1, violations.size(), "should be 1 exception");
}
}
Spring Validation
Spring supports Java’s Bean Validation API, This makes it easy to declare validation rules as opposed to explicitly writing declaration logic in your application code.
Spring provides a built-in validation API by way of the Validator
interface. This interface allowing you to encapsulate your validation logic into a class responsible for validating the target object. In addition to the target object, the validate method takes an Errors
object, which is used to collect any validation errors that may occur.
Spring also provides a handy utility class, ValidationUtils, which provides convenience methods for invoking other validators, checking for common problems such as empty strings, and reporting errors back to the provided Errors object.
LocalValidatorFactoryBean is a Spring-managed bean since Spring 3.0.
How to import validation package
- If your spring-boot version is less than 2.3.x,
spring-boot-starter-web
has includedhibernate-validator
so you don’t need to import other packages. - otherwise, you need to manually import either of the following packages:
hibernate-validator
spring-boot-starter-validation
Where should the validation happen
Generally speaking, there are three layers we would like to implement validation:
- presentation layer, we must validate the DTO
- Service layer, something we need to do validation
- persistence layer, defitely you don’t want to save some bad data
02. DTO Bean Validation in Spring, using @Valid
For now, we would like to know how Java Bean validation works in Spring.
Later, in the next chapter, we’ll introduce the parameter validation in Spring. Parameters are not java beans, so you cannot use bean validation against them.
The above two validations would throw different kinds of exceptions, we’ll cover it in a separate chapter.
Note that Spring MVC validates ViewModels (VM is not DTO) and put results to a BindResult, I don’t have a plan to mention it in the following parts.
Request body Validation with Bean Validation and Spring Validation
-
use Bean Validation, add
@Valid
in front of@RequestBody
works. It validates all the fields before throwing exceptions. Invalid Request body throwsMethodArgumentNotValidException
.@PostMapping ResponseEntity<ToDoDto> createTodo(@Valid @RequestBody ToDoDto toDoDto) { return ResponseEntity.ok(toDoDto); } @ExceptionHandler({MethodArgumentNotValidException.class}) public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { HttpHeaders headers = new HttpHeaders(); String ResponseBody = "Response Body, should be a JSON"; return new ResponseEntity(ResponseBody, headers, HttpStatus.BAD_REQUEST); }
-
use Spring Validation to validate Request Body
- class level
@Validated
doesn’t help with Request Body validation - Add
@Valdaited
in front of@RequestBody
works, we’ll explain why both of them work - Without Spring data-binding and validation errors, it throws
MethodArgumentNotValidException
and goes to 400 error
@PostMapping ResponseEntity<ToDoDto> createTodo(@Validated @RequestBody ToDoDto toDoDto) { return ResponseEntity.ok(toDoDto); } @ExceptionHandler({MethodArgumentNotValidException.class}) public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { HttpHeaders headers = new HttpHeaders(); String ResponseBody = "Response Body, should be a JSON"; return new ResponseEntity(ResponseBody, headers, HttpStatus.BAD_REQUEST); }
- class level
-
Use Spring validation error binding and validation errors. You can take advantage of the errors object, it cotnains the filed and error message, so that you can return the error message to the API caller. It works for both Bean validation and Spring validation. It’s from package
org.springframework.validation
@PostMapping
ResponseEntity<ToDoDto> createTodo(@Valid @RequestBody ToDoDto toDoDto, Errors errors) {
if (errors.hasErrors()) {
throw new RuntimeException("Please handle these validation exceptions");
}
return ResponseEntity.ok(toDoDto);
}
@PostMapping
ResponseEntity<ToDoDto> createTodo(@Validated @RequestBody ToDoDto toDoDto, Errors errors) {
if (errors.hasErrors()) {
throw new RuntimeException("Please handle these validation exceptions");
}
return ResponseEntity.ok(toDoDto);
}
Testing with DTO Bean Validation
Note that you should customize the error message. We will do this in another chapter.
@PostMapping
ResponseEntity<ToDoDto> createTodo(@Valid @RequestBody ToDoDto toDoDto) {
return ResponseEntity.ok(toDoDto);
}
@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
HttpHeaders headers = new HttpHeaders();
String ResponseBody = "Response Body, should be a JSON";
return new ResponseEntity(ResponseBody, headers, HttpStatus.BAD_REQUEST);
}
@Test
void createToDoWithInvalidEmail() throws Exception {
ToDoDto dto = new ToDoDto();
dto.setEmail("example@@");
dto.setPostCode(1234);
dto.setContent("exdample");
String body = objectMapper.writeValueAsString(dto);
mockMvc.perform(post("/")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest())
.andExpect(content().string("Response Body, should be a JSON"));
}
@Test
void createToDoWithValidEmail() throws Exception {
ToDoDto dto = new ToDoDto();
dto.setEmail("example@example.com");
dto.setPostCode(1234);
dto.setContent("example");
String body = objectMapper.writeValueAsString(dto);
mockMvc.perform(post("/")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk());
}
03. Spring Parameter validation, introducing @Validated
Parameters are not JavaBeans, so Bean Validation doesn’t help. Spring implements parameter validation by itself.
The keys are:
- add
@Validated
to the class you want to validate parameters. Without it, parameter validation is not enabled - you don’t need to add
@Valid
to the parameter - it throws
ConstraintViolationException
if the parameter is invalid - it works for both
RequestParam
andPathVariable
-
you cannot use Errors Binding here, otherwise, you’ll get an
IllegalStateException
java.lang.IllegalStateException: An Errors/BindingResult argument is expected to be declared immediately after the model attribute, the @RequestBody or the @RequestPart arguments to which they apply: org.springframework.http.ResponseEntity com.mg.todo.ToDoController.fetchByEmail( java.lang.String,org.springframework.validation.Errors)
Example and test case
import org.springframework.validation.annotation.Validated;
@Validated
@RestController
public class ToDoController {
private final ToDoService toDoService;
public ToDoController(ToDoService toDoService) {
this.toDoService = toDoService;
}
@GetMapping
ResponseEntity<List<ToDoDto>> fetchByEmail(@RequestParam("email") @Email(message = "should be a valid email") String email) {
return ResponseEntity.ok(new ArrayList<ToDoDto>());
}
@ExceptionHandler({ConstraintViolationException.class})
public ResponseEntity handleConstrainViolationException() {
HttpHeaders headers = new HttpHeaders();
String ResponseBody = "should be a valid email";
return new ResponseEntity(ResponseBody,headers, HttpStatus.BAD_REQUEST );
}
}
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ToDoController.class)
class ToDoControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
ToDoService toDoService;
@Test
void fetchByValidEmail() throws Exception {
mockMvc.perform(get("/")
.param("email","valid@example.com")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isOk());
}
@Test
void fetchByInvalidEmail() throws Exception {
mockMvc.perform(get("/")
.param("email","invalid@@")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isBadRequest())
.andExpect(content().string("should be a valid email"));
}
}
You can also use @Validated for Request Body DTO validation
In the background, Spring wraps Hibernate-Validator
and does the validation work.
Note that resolveArgument()
method calls validateIfApplicable(binder, parameter);
and this is the key.
It picks up @Validate
parameters first and it triggers validation.
If it’s absent, it looks for any parameters’ annotation that start from Valid
.
That’s why @Validated
works for both DTO and parameters and DTO here.
package org.springframework.web.servlet.mvc.method.annotation;
/**
* Resolves method arguments annotated with {@code @RequestBody} and handles return
* values from methods annotated with {@code @ResponseBody} by reading and writing
* to the body of the request or response with an {@link HttpMessageConverter}.
*/
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
/**
* Throws MethodArgumentNotValidException if validation fails.
* @throws HttpMessageNotReadableException if {@link RequestBody#required()}
* is {@code true} and there is no body content or if there is no suitable
* converter to read the content with.
*/
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
}
package org.springframework.web.servlet.mvc.method.annotation;
/**
* A base class for resolving method argument values by reading from the body of
* a request with {@link HttpMessageConverter HttpMessageConverters}.
*/
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
/**
* Validate the binding target if applicable.
* <p>The default implementation checks for {@code @javax.validation.Valid},
* Spring's {@link org.springframework.validation.annotation.Validated},
* and custom annotations whose name starts with "Valid".
*/
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
binder.validate(validationHints);
break;
}
}
}
}
04. Handle Validation Errors
In the above examples, we would know:
- Bean validation throws
MethodArgumentNotValidException
- If we use parameter Error Binding, we can make use of the
errors
object. - You can get each error by
ex.getBindingResult().getFieldErrors()
- If we use parameter Error Binding, we can make use of the
- Spring parameter validation throws
ConstraintViolationException
,- it cannot use parameter Error Binding.
- You can get each error by
e.getConstraintViolations()
- There actually is another exception
org.springframework.validation.BindException
, thrown in MVC form submitContent-Type: multipart/form-data
, we didn’t mention this but you should know it exists.
A new issue comes out. How can we format/unify our error response body, so that the API caller would have a meaningful error message.
- We should always return a unified Response entity for exceptions. Use
ResponseStatusException
- We could make use of
@RestControllerAdvice
to handle exception globally
Returns Unified error response
this content is from All You Need To Know About Bean Validation With Spring Boot
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ValidationErrorResponse {
private List<Violation> violations = new ArrayList<>();
}
@Data
@AllArgsConstructor
class Violation {
private String fieldName;
private String message;
}
@ControllerAdvice
public class GlobalRestExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
ValidationErrorResponse onMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
ValidationErrorResponse error = new ValidationErrorResponse();
for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
error.getViolations().add(
new Violation(fieldError.getField(), fieldError.getDefaultMessage()));
}
return error;
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
ValidationErrorResponse onConstraintValidationException(
ConstraintViolationException e) {
ValidationErrorResponse error = new ValidationErrorResponse();
for (ConstraintViolation violation : e.getConstraintViolations()) {
error.getViolations().add(
new Violation(violation.getPropertyPath().toString(), violation.getMessage()));
}
return error;
}
}
@Test
void createToDoWithInvalidEmail() throws Exception {
ToDoDto dto = new ToDoDto();
dto.setEmail("example@@");
dto.setPostCode("0060");
String body = objectMapper.writeValueAsString(dto);
final MvcResult mvcResult = mockMvc.perform(post("/")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andReturn();
final String content = mvcResult.getResponse().getContentAsString();
Assertions.assertNotNull(content);
Assertions.assertTrue(content.contains("\"fieldName\":\"content\",\"message\":\"Content cannot be empty\""),"should contain this text");
Assertions.assertTrue(content.contains("\"fieldName\":\"email\",\"message\":\"Please provide a valid Email\""),"should contain this text");
}
05. @Valid vs @Validated, validation groups using @Validated
Oftentimes, you need to validate a filed differently based on the scenario. A common case, we don’t have an Id for an object on creating, however, we do need a valid id while updating. The javax.validation
@Valid
doesn’t support Groups. The Spring @Validated
does.
Steps:
- Add groups to the Model
- Use
@Validated
and Groups in Controller - MockMVC testing
Use Validation Groups
@Data
public class ToDoDto implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull(message = "Id should not be null on updating", groups = Update.class)
private UUID uuid;
@Email(message = "Please provide a valid Email", groups = {Create.class, Update.class})
private String email;
@Range(min = 13, max = 13, message = "Please provide a valid phone number. E.g., 0064-01-234-5678", groups = {Create.class, Update.class})
private Integer phoneNumber;
@Digits(integer = 4, fraction = 0, message = "Please provide a valid post code", groups = {Create.class, Update.class})
private String postCode;
@NotBlank(message = "Content cannot be empty", groups = {Create.class, Update.class})
private String content;
@Future(groups = {Create.class, Update.class})
private LocalDateTime due;
//on creating
public interface Create { }
// on updating
public interface Update { }
}
@PostMapping
ResponseEntity<ToDoDto> createTodo(@Validated(ToDoDto.Create.class) @RequestBody ToDoDto toDoDto) {
return ResponseEntity.ok(toDoDto);
}
@Test
void updateToDoWithUUID() throws Exception {
ToDoDto dto = new ToDoDto();
dto.setEmail("example@example.com");
dto.setPostCode("0060");
dto.setContent("example");
dto.setUuid(UUID.randomUUID());
String body = objectMapper.writeValueAsString(dto);
mockMvc.perform(put("/")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON));
}
@Test
void updateToDoWithoutUUID() throws Exception {
ToDoDto dto = new ToDoDto();
dto.setEmail("example@example.com");
dto.setPostCode("0060");
dto.setContent("example");
String body = objectMapper.writeValueAsString(dto);
final MvcResult mvcResult = mockMvc.perform(put("/")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andReturn();
String content = mvcResult.getResponse().getContentAsString();
Assertions.assertTrue(content.contains( "violations"));
Assertions.assertTrue(content.contains("Id should not be null on updating"));
}
06. @Valid vs @Validated, Collection validation using @Valid
In the Demo, all fields we validate are Java provided types, e.g., String, Long or LocalDateTime. However, it’s not always the case.
- we may need to validate another object nested in your DTO
- We may need to validate a list of your DTOs
Validate Nested object
@Data
public class ToDoDto implements Serializable {
// .... other fields
@NotNull(groups = {Create.class, Update.class})
@Valid
private MetaData metaData;
@Data
public static class MetaData {
@Min(value = 1, groups = Update.class)
private Long Id;
@NotNull(groups = {Create.class, Update.class})
@Length(min = 2, max = 10, groups = {Create.class, Update.class})
private String Name;
@NotNull(groups = {Create.class, Update.class})
@Length(min = 2, max = 10, groups = {Create.class, Update.class})
private String position;
}
public interface Create { }
public interface Update { }
In our case, if you have an empty Meta data object, you’ll get the following error mesasge:
{"violations":[
{"fieldName":"metaData.Name","message":"must not be null"},
{"fieldName":"metaData.position","message":"must not be null"}
]}
Validate a list of DTOs
- In
java.util.Collection
,List
andSet
don’t work for validation - We need to implement our own collection, e.g.,
myValidationList
to accept the data and do validation
Bean Validation doesn’t work in List of DTOs
@PostMapping(value = "/todos")
ResponseEntity<List<ToDoDto>> createTodos(@Validated(ToDoDto.Create.class) @RequestBody List<ToDoDto> toDoDtos) {
return ResponseEntity.ok(toDoDtos);
}
@Test
void createToDosWithInvalidEmailShouldBe400ButIs200() throws Exception {
ToDoDto dto = new ToDoDto();
dto.setEmail("example@@");
dto.setPostCode("0060");
ToDoDto dto2 = new ToDoDto();
dto2.setEmail("example@@");
dto2.setPostCode("0060");
String body = objectMapper.writeValueAsString(List.of(dto, dto2));
mockMvc.perform(post("/todos")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk());
}
Implement MyValidationList for List of DTOs
It throws org.springframework.beans.NotReadablePropertyException
if there is any violation
public class MyValidationList<E> implements List<E> {
@Delegate // lombok annotation
@Valid
public List<E> list = new ArrayList<>();
}
@PostMapping(value = "/todos2")
ResponseEntity<List<ToDoDto>> createTodos2(@Validated(ToDoDto.Create.class) @RequestBody MyValidationList<ToDoDto> toDoDtos) {
return ResponseEntity.ok(toDoDtos);
}
@ExceptionHandler({NotReadablePropertyException.class})
public ResponseEntity handleMethodArgumentNotValidException(NotReadablePropertyException e) {
HttpHeaders headers = new HttpHeaders();
String ResponseBody = e.getMessage();
return new ResponseEntity(ResponseBody, headers, HttpStatus.BAD_REQUEST);
}
@Test
void createToDosWithMyValidationListOfDtosWithInvalidEmail() throws Exception {
ToDoDto dto = new ToDoDto();
dto.setEmail("example@@");
dto.setPostCode("0060");
ToDoDto dto2 = new ToDoDto();
dto2.setEmail("example@@");
dto2.setPostCode("0060");
String body = objectMapper.writeValueAsString(List.of(dto, dto2));
mockMvc.perform(post("/todos2")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest());
}
@Test
void createToDosWithMyValidationListOfDtos() throws Exception {
ToDoDto dto = new ToDoDto();
dto.setEmail("example@example.com");
dto.setPostCode("0060");
dto.setContent("example");
dto.setUuid(UUID.randomUUID());
final ToDoDto.MetaData metaData = new ToDoDto.MetaData();
metaData.setName("name");
metaData.setPosition("position");
dto.setMetaData(metaData);
ToDoDto dto2 = new ToDoDto();
dto2.setEmail("example@example.com");
dto2.setPostCode("0060");
dto2.setContent("example");
dto2.setUuid(UUID.randomUUID());
final ToDoDto.MetaData metaData2 = new ToDoDto.MetaData();
metaData2.setName("name");
metaData2.setPosition("position");
dto2.setMetaData(metaData2);
String body = objectMapper.writeValueAsString(List.of(dto));
mockMvc.perform(post("/todos2")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk());
}
07. Declarative customized annotation validation
Again, in a real scenario, we may need more complicated logic than what Spring provides us.
Steps:
- Add the new annotation interface
- Add a customized validator and implement
ConstraintValidator
- Add this new annotation to the DTO
- No need to do anything to the Controller or Service layer
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface NotGmail {
String message() default "We don't support Gmail";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class NotGmailValidator implements ConstraintValidator<NotGmail, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value != null) {
return !value.toLowerCase().contains("@gmail");
}
return true;
}
}
@Data
public class ToDoDto implements Serializable {
@Email(message = "Please provide a valid Email", groups = {Create.class, Update.class})
@com.mg.todo.dto.NotGmail(message = "Sorry, no Google", groups = {Create.class, Update.class})
private String email;
}
@Test
void createToDoWithGoogleEmailShouldBeInvalid() throws Exception {
ToDoDto dto = new ToDoDto();
dto.setEmail("xiguadawang@gmail.com");
dto.setPostCode("0060");
dto.setContent("example");
final ToDoDto.MetaData metaData = new ToDoDto.MetaData();
metaData.setName("name");
metaData.setPosition("position");
dto.setMetaData(metaData);
String body = objectMapper.writeValueAsString(dto);
final MvcResult mvcResult = mockMvc.perform(post("/")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andReturn();
final String content = mvcResult.getResponse().getContentAsString();
Assertions.assertTrue(content.contains("{\"violations\":[{\"fieldName\":\"email\",\"message\":\"Sorry, no Google\"}]}"),"should contain this text");
}
08. Programmatic validator validation
Spring Boot provides us with a pre-configured Validator instance, and you can always explicitly reference the javax.validation.Validator
.
It doesn’t need the @Validated
key world on the class name.
A scenario like when a new user registers. When all fields are good, we still need to validate the username is occupied or not. We use declarative validation for the correctness of the email and we need to issue a database call to check the username which should be a programmatic validation.
Spring Validator and Javax Validator works the same way
@RestController
public class ToDoController {
@Qualifier("defaultValidator")
@Autowired
private org.springframework.validation.Validator springValidator;
@Autowired
private javax.validation.Validator globalValidator;
@PostMapping(value = "/javaxValidator")
ResponseEntity<ToDoDto> createTodos3(@RequestBody ToDoDto toDoDto) {
Set<ConstraintViolation<ToDoDto>> violations = globalValidator.validate(toDoDto, ToDoDto.Create.class);
if (violations.isEmpty()) {
// passed the validation, free to go
} else {
for (ConstraintViolation<ToDoDto> userDTOConstraintViolation : violations) {
// Failed the validation, do your business here. E.g., send the error to a Queue
}
}
return new ResponseEntity("Failed for javaxValidator", new HttpHeaders(), HttpStatus.BAD_REQUEST);
}
@PostMapping(value = "/springValidator")
ResponseEntity<ToDoDto> createTodos4(@RequestBody ToDoDto toDoDto) {
Set<ConstraintViolation<ToDoDto>> violations = globalValidator.validate(toDoDto, ToDoDto.Create.class);
if (violations.isEmpty()) {
// passed the validation, free to go
} else {
for (ConstraintViolation<ToDoDto> userDTOConstraintViolation : violations) {
// Failed the validation, do your business here. E.g., send the error to a Queue
}
}
return new ResponseEntity("Failed for spring Validator", new HttpHeaders(), HttpStatus.BAD_REQUEST);
}
}
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ToDoController.class)
class ToDoControllerTest {
@Autowired
private javax.validation.Validator globalValidator;
@Qualifier("defaultValidator")
@Autowired
private org.springframework.validation.Validator springValidator;
@Test
void createToDosWithJavaxValidatorAndGoogleEmailShouldReturnBadRequest() throws Exception {
ToDoDto dto = new ToDoDto();
dto.setEmail("xiguadawang@gmail.com");
dto.setPostCode("0060");
dto.setContent("example");
dto.setUuid(UUID.randomUUID());
final ToDoDto.MetaData metaData = new ToDoDto.MetaData();
metaData.setName("name");
metaData.setPosition("position");
dto.setMetaData(metaData);
String body = objectMapper.writeValueAsString(dto);
final MvcResult mvcResult = mockMvc.perform(post("/javaxValidator")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest())
.andReturn();
final String content = mvcResult.getResponse().getContentAsString();
Assertions.assertEquals("Failed for javaxValidator", content);
}
@Test
void createToDosWithSpringValidatorGmailShouldReturnBadRequest() throws Exception {
ToDoDto dto = new ToDoDto();
dto.setEmail("xiguadawang@gmail.com");
dto.setPostCode("0060");
dto.setContent("example");
dto.setUuid(UUID.randomUUID());
final ToDoDto.MetaData metaData = new ToDoDto.MetaData();
metaData.setName("name");
metaData.setPosition("position");
dto.setMetaData(metaData);
String body = objectMapper.writeValueAsString(dto);
final MvcResult mvcResult = mockMvc.perform(post("/springValidator")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest())
.andReturn();
final String content = mvcResult.getResponse().getContentAsString();
Assertions.assertEquals("Failed for spring Validator", content);
}
}
Configure Validator Bean in Spring
You can configure a Validator bean by yourself. By default, it uses HibernateValidator.class
, there are other options like ApacheValidationProvider.class
.
A benefit of configuring it is to make it fail fast.
In failFast mode, it throws an exception on the first violation.
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
return validatorFactory.getValidator();
09. Service layer validation needs both @Validate and @Valid
@Service
@Validated
class ValidatingService{
void validateInput(@Valid DTO dto){
// do something
}
}
10. Persistence layer validation
- By default, Spring uses Hibernate-Validator by default so that it supports Bean Validation out of the box
- The validation happends on the repository save or update method
- No need to add any annotation to the Repository
- It throws
ConstraintViolationException
on violations