Bean validation in Spring Rest using @Valid:
Bean validation is important to validate the input bean received in the spring restful requestbody. And also default error messages may not be properly understandable by our consumers, so validating the bean and responding the proper readable message to our consumers makes our spring restful webservice better.
Table of Contents
How are we going to do ?
1. Add the validation annotations to the bean fields(preferrably your Dto class), following are the supported annotations for validations:
- @DecimalMax
- @DecimalMin
- @Digits
- @Future
- @FutureOrPresent
- @Max
- @Min
- @Negative
- @NegativeOrZero
- @NotBlank
- @NotEmpty
- @NotNull
- @Null
- @Past
- @PastOrPresent
- @Pattern
- @Positive
- @PositiveOrZero
2. Create custom exception handler class and custom error details class to handle and response the errors
3. Add @Valid to all the request methods where you need to perform bean validations.
Step 1: Add @Valid to controller method body
In Controller class, add @valid annotation to method arguments.
[java]
@RestController
@RequestMapping(“/coupons”)
public class CouponController {
@RequestMapping(value = “/create”, method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Boolean> createCoupon(@Valid @RequestBody CouponDTO couponDTO) {
try {
// TODO: Logic to insert/save/create new coupon in db.
return new ResponseEntity<Boolean>(Boolean.TRUE, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<Boolean>(Boolean.FALSE, HttpStatus.EXPECTATION_FAILED);
}
}
}
[/java]
Step 2: Create a custom ResponseEntityExceptionHandler for bean validation exception handlings:
[java]
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.springframework.http.HttpHeaders;
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.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
@RestController
public class JdResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
JdResponseErrors errorDetails = new JdResponseErrors(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<Object>(errorDetails, HttpStatus.BAD_REQUEST);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
List<JdResponseErrors> beanValidationErrors = new ArrayList<JdResponseErrors>();
for (FieldError fr : ex.getBindingResult().getFieldErrors()) {
JdResponseErrors errorDetails = new JdResponseErrors(new Date(), “Validation Failed”,
fr.getDefaultMessage());
beanValidationErrors.add(errorDetails);
}
return new ResponseEntity<Object>(beanValidationErrors, HttpStatus.BAD_REQUEST);
}
}
[/java]
Step 3: Custom response error POJO – JdResponseErrors.java
It is just a response error class, keep the fields you want for the response errors.
[java]
import java.util.Date;
import org.springframework.validation.FieldError;
public class JdResponseErrors {
private Date timestamp;
private String message;
private String details;
private FieldError fieldError;
public JdResponseErrors(Date timestamp, String message, String details) {
super();
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
public JdResponseErrors(Date timestamp2, String message2, FieldError fieldError) {
super();
this.timestamp = timestamp2;
this.message = message2;
this.fieldError = fieldError;
}
public Date getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getDetails() {
return details;
}
}
[/java]
Step 4: CouponDTO – place where to add the validations:
Add needed validations to needed fields with the error messages. Same message only will be returned when the particular field validation is failed.
[java]
public class CouponDTO {
private long couponId;
@Size(min = 30, message = “Coupon title should be minimum 30”)
@NotNull(message = “Coupon title should not be null”)
@NotEmpty(message = “Coupon title should not be empty”)
private String couponTitle;
@Size(min = 1, max = 1, message = “Expired field should not be more than 1 character”)
@Pattern(regexp = “(?:Y|N)”, message = “Expired field should be Y/N only.”)
private String isExpired;
// getters & setters
}
[/java]
Sample Request 1:
[plain]
{“couponTitle”:”20% Offer On Mobiles”,”isExpired”:”Yes”}
[/plain]
Sample Response 1:
[
{
“timestamp”: 1526990746099,
“message”: “Validation Failed”,
“details”: “Expired field should be Y/N only.”
}
]
Sample Request 2:
[plain]
{“couponTitle”:””,”isExpired”:”Yes”}
[/plain]
Sample Response 2:
[
{
“timestamp”: 1526990746099,
“message”: “Validation Failed”,
“details”: “Coupon title should not be empty”
},
{
“timestamp”: 1526990746099,
“message”: “Validation Failed”,
“details”: “Expired field should be Y/N only.”
}
]
Below is my observation which I felt can be improved in validations, feel free to add your comments as well:
I could not find any annotation for validating the datatype. For example, string fields should not be entered with any number then, currently other than pattern matching no other way is there.