The Bean Validation API is used for the validation of constraints on an object model using annotations. It can be very useful for validating the length, format, regular expressions, etc. of an object model. By default, bean validation is a Java specification; we need to explicitly provide its implementation.
Basics of Bean Validation API
- Java Bean validations are done with JSR 380 which is known as Bean Validation 2.0
- JSR 380 is the specification of the Java Bean Validation API, and we need to provide its implementation
- Bean validation using the Bean Validation API is done with annotations such as @NotNull, @Min, @Max, @Size, @Email, @NotEmpty, etc.
- Java Bean Validation API is the specification and Hibernate Validator is the implementation of Java Bean Validation API
In our previous tutorial, How to create a REST API | User API, we developed our User API, through which we can perform the following operations on a user entity: i.e., create, update, fetch, and delete. When we try to perform CRUD operations without using validations, we face inappropriate behaviour from our application. For example, if we try to add a user and put null values in name or don’t use @ in email, we are still able to make the request and add data to the database, which is not a good practice for writing restful services.
We can also check our database, which is filled with null values.
In order to resolve this issue, we will use Bean Validation such that when the validation criteria are not met, it will pop up a custom message for fulfilling the validation criteria.
Steps By Step Implementations
Step 1- Add Dependencies
In this step, we need to add model mapper dependencies. For this, we will be using the Central Maven Repository. We will add the following dependency.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
Step 2- Validate UserDto Class
In this step, we will use bean validation api’s to validate our userDto (Model class), as we are using the userDto object to send and receive the request and response from the client (here, Postman). For this, go to DTO Package > UserDto Class and add the following validations to it.
package com.paulsofts.blogapplicationservices.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/*
* UserDTO class will be used to transfer data and
* can be exposed to web-layer
*
* User class, we will use only for performing database
* related operations
* */
@Getter
@Setter
@NoArgsConstructor
public class UserDto {
private int userId;
//we have used bean validation api to validate the fields
@NotEmpty
@Size(min = 4, message = "Username must contains at least 4 characters")
private String userName;
@Email(message = "Email address is not valid")
private String userEmail;
//min and max size userPassword should have and if not
//what will be the pop up message
@NotEmpty
@Size(min = 8, max = 15, message = "Password must contains at least 8 characters")
private String userPassword;
@NotEmpty
private String userAbout;
}
Step 3- Add @Valid Annotation In UserController
We request our business logic (here, the UserService class) using the controller class, so we need to add @Valid annotations in our controller class (here, the UserController class). We have added the @Valid annotation to our createUser() and updateUser() methods because there we are passing our UserDto object, which needs to be validated.
package com.paulsofts.blogapplicationservices.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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 com.paulsofts.blogapplicationservices.dto.UserDto;
import com.paulsofts.blogapplicationservices.service.UserServiceImpl;
import com.paulsofts.blogapplicationservices.utils.GenericRequest;
import com.paulsofts.blogapplicationservices.utils.GenericResponse;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserServiceImpl userServiceImpl;
//it's just a welcome api to check whether application is running or not
@GetMapping("/welcome")
public String welcome() {
return "Welcome to paulsofts";
}
//we will be using custom GenericRequest<T> class to request the services
//which will take the custom type of argument, here it is taking UserDto
//similarly, we will be using custom GenericResponse<T> class to return the response
//it will take three argument- GenericType, message and response
//which we have entered in the constructor of GenericResponse<T>
@PostMapping("/create")
public ResponseEntity<GenericResponse<UserDto>> createUser(@Valid @RequestBody GenericRequest<UserDto> request){
UserDto createdUserDto = this.userServiceImpl.createUser(request.getT());
return new ResponseEntity<GenericResponse<UserDto>>(new GenericResponse<UserDto>(createdUserDto, "user created", "OK"), HttpStatus.CREATED);
}
@PostMapping("/update/{userId}")
public ResponseEntity<GenericResponse<UserDto>> updateUser(@Valid @RequestBody GenericRequest<UserDto> request, @PathVariable("userId") int userId){
UserDto updatedUserDto = this.userServiceImpl.updateUser(request.getT(), userId);
return new ResponseEntity<GenericResponse<UserDto>>(new GenericResponse<UserDto>(updatedUserDto, "user updated", "OK"), HttpStatus.OK);
}
@GetMapping("/get/{userId}")
public ResponseEntity<GenericResponse<UserDto>> getUserById(@PathVariable("userId") int userId){
UserDto userDto = this.userServiceImpl.getUserById(userId);
return new ResponseEntity<GenericResponse<UserDto>>(new GenericResponse<UserDto>(userDto, "user", "OK"), HttpStatus.OK);
}
@GetMapping("/get")
public ResponseEntity<GenericResponse<List<UserDto>>> getAllUser(){
List<UserDto> userDtoList = this.userServiceImpl.getAllUser();
return new ResponseEntity<GenericResponse<List<UserDto>>>(new GenericResponse<List<UserDto>>(userDtoList, "user list", "OK"), HttpStatus.OK);
}
//as return type of deleteUser() method of service class is void
//we will be using String as type of our GenericResponse class
@DeleteMapping("/delete/{userId}")
public ResponseEntity<GenericResponse<String>> deleteUser(@PathVariable("userId") int userId){
this.userServiceImpl.deleteUser(userId);
return new ResponseEntity<GenericResponse<String>>(new GenericResponse<String>("userId " + userId, "user deleted", "OK"), HttpStatus.OK);
}
}
As we have used GenericRequest<T>, we also need to add the @Valid annotation to our GenericRequest<T> type.
package com.paulsofts.blogapplicationservices.utils;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class GenericRequest<T> {
//we need to add @Valid annotation to our Generic Type Object
@Valid
private T t;
}
Step 4- Run Blog Application
In this step, we will run our application to check whether our bean validation is working. For this, go to Main Class (here, BlogApplicationServicesApplication) > Right-Click > Run As > Java Application, and we will try to create a new user with invalid data.
Below, we have a snippet of the complete response. As we can see in the error[] array, we have the validation message that we have used in our UserDto class. But it is not in generic form. We need to convert the message into generic forms that are easy to read and understand.
{
"timestamp": "2023-03-03T07:16:15.663+00:00",
"status": 400,
"error": "Bad Request",
"trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<com.paulsofts.blogapplicationservices.utils.GenericResponse<com.paulsofts.blogapplicationservices.dto.UserDto>> com.paulsofts.blogapplicationservices.controller.UserController.createUser(com.paulsofts.blogapplicationservices.utils.GenericRequest<com.paulsofts.blogapplicationservices.dto.UserDto>) with 4 errors: [Field error in object 'genericRequest' on field 't.userPassword': rejected value [paul]; codes [Size.genericRequest.t.userPassword,Size.t.userPassword,Size.userPassword,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [genericRequest.t.userPassword,t.userPassword]; arguments []; default message [t.userPassword],15,8]; default message [Password must contains at least 8 characters]] [Field error in object 'genericRequest' on field 't.userEmail': rejected value [paulsoftsgmail.com]; codes [Email.genericRequest.t.userEmail,Email.t.userEmail,Email.userEmail,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [genericRequest.t.userEmail,t.userEmail]; arguments []; default message [t.userEmail],[Ljakarta.validation.constraints.Pattern$Flag;@6566e8be,.*]; default message [Email address is not valid]] [Field error in object 'genericRequest' on field 't.userName': rejected value []; codes [Size.genericRequest.t.userName,Size.t.userName,Size.userName,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [genericRequest.t.userName,t.userName]; arguments []; default message [t.userName],2147483647,4]; default message [Username must contains at least 4 characters]] [Field error in object 'genericRequest' on field 't.userName': rejected value []; codes [NotEmpty.genericRequest.t.userName,NotEmpty.t.userName,NotEmpty.userName,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [genericRequest.t.userName,t.userName]; arguments []; default message [t.userName]]; default message [must not be empty]] \r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:144)\r\n\tat org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:181)\r\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:148)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884)\r\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)\r\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1080)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:973)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:731)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:814)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:400)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:859)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1734)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.base/java.lang.Thread.run(Thread.java:833)\r\n",
"message": "Validation failed for object='genericRequest'. Error count: 4",
"errors": [
{
"codes": [
"Size.genericRequest.t.userPassword",
"Size.t.userPassword",
"Size.userPassword",
"Size.java.lang.String",
"Size"
],
"arguments": [
{
"codes": [
"genericRequest.t.userPassword",
"t.userPassword"
],
"arguments": null,
"defaultMessage": "t.userPassword",
"code": "t.userPassword"
},
15,
8
],
"defaultMessage": "Password must contains at least 8 characters",
"objectName": "genericRequest",
"field": "t.userPassword",
"rejectedValue": "paul",
"bindingFailure": false,
"code": "Size"
},
{
"codes": [
"Email.genericRequest.t.userEmail",
"Email.t.userEmail",
"Email.userEmail",
"Email.java.lang.String",
"Email"
],
"arguments": [
{
"codes": [
"genericRequest.t.userEmail",
"t.userEmail"
],
"arguments": null,
"defaultMessage": "t.userEmail",
"code": "t.userEmail"
},
[],
{
"arguments": null,
"defaultMessage": ".*",
"codes": [
".*"
]
}
],
"defaultMessage": "Email address is not valid",
"objectName": "genericRequest",
"field": "t.userEmail",
"rejectedValue": "paulsoftsgmail.com",
"bindingFailure": false,
"code": "Email"
},
{
"codes": [
"Size.genericRequest.t.userName",
"Size.t.userName",
"Size.userName",
"Size.java.lang.String",
"Size"
],
"arguments": [
{
"codes": [
"genericRequest.t.userName",
"t.userName"
],
"arguments": null,
"defaultMessage": "t.userName",
"code": "t.userName"
},
2147483647,
4
],
"defaultMessage": "Username must contains at least 4 characters",
"objectName": "genericRequest",
"field": "t.userName",
"rejectedValue": "",
"bindingFailure": false,
"code": "Size"
},
{
"codes": [
"NotEmpty.genericRequest.t.userName",
"NotEmpty.t.userName",
"NotEmpty.userName",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"genericRequest.t.userName",
"t.userName"
],
"arguments": null,
"defaultMessage": "t.userName",
"code": "t.userName"
}
],
"defaultMessage": "must not be empty",
"objectName": "genericRequest",
"field": "t.userName",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"path": "/api/users/create"
}
Step 5- Handling MethodArgumentNotValidException
In the above response, we can see the trace message that indicates we are getting a MethodArgumentNotValidException. We need to handle it as we have handled ResourceNotFoundException and return a custom response that includes only validation messages. For this, go to Utils Package > GlobalExceptionHandler Class and the following code for handling MethodArgumentNotValidException.
package com.paulsofts.blogapplicationservices.utils;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
//we have used return type of GenericResponse<T> class and
//this method will be called whereever we will throw
//our custom ResourceNotFoundException
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<GenericResponse<String>> resourceNotFoundExceptionHandler(ResourceNotFoundException resourceNotFoundException){
//reading message from ResourceNotFoundException class instance
String message = resourceNotFoundException.getMessage();
return new ResponseEntity<GenericResponse<String>>(
new GenericResponse<String>(message, "Not Found", "False"), HttpStatus.NOT_FOUND);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> methodArgumentNotValidException(MethodArgumentNotValidException methodArgumentNotValidException) {
Map<String, String> response = new HashMap<>();
//we are taking out list of errors from methodArgumentNotValidException
//and iterating the list and getting the error message, and error field
//and storing it in a map and then returning the map as response entity
methodArgumentNotValidException.getBindingResult().getAllErrors().forEach((error)->{
String fieldName = ((FieldError)error).getField();
String errorMessage = error.getDefaultMessage();
response.put(fieldName, errorMessage);
});
return new ResponseEntity<Map<String,String>>(response, HttpStatus.BAD_REQUEST);
}
}
Step 6- Test Blog Application
In this step, we will again run our application, try to create a new user with invalid data, and check for our custom validation messages.