Home » Framework » Custom Validator and Handling Errors in Spring Boot

Custom Validator and Handling Errors in Spring Boot

The existing constraint of spring boot is not enough to validate the bean of some fields. In this situation, spring provides the option to create our own validator that is a custom validator.

This article lets me explain how to create a custom annotation to validate the mobile number and email ID. The custom annotation @Mobile validates the mobile number and the annotation @ValidEmail validates the email ID.

Create custom annotation to validate Mobile Number

First, we have to create custom annotation @Mobile to validate the mobile number. The mobile number should be 10 digits and it does not contain an alphabet.

Before that here we take the 'Profile' entity to apply the custom annotations with the fields of mobileNumber and emailId. The snippet of the profile entity is below.

package com.tipstocode.model;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import com.tipstocode.validator.custom.Mobile;
import com.tipstocode.validator.custom.ValidEmail;
public class Profile {	
	//Fields
	@NotBlank
	private String firstName;	
	@NotBlank
	private String lastName;	
	@NotBlank
	@NotNull
	@Mobile
	private String mobileNumber;	
	@NotBlank
	@ValidEmail
	@NotNull
	private String emailId;			
	//Constructor	
	public Profile(){}	
	public Profile(String firstName, String lastName, String mobileNumber,
			String emailId) {
		super();
		this.firstName = firstName;
		this.lastName = lastName;
		this.mobileNumber = mobileNumber;
		this.emailId = emailId;		
	}	
	//Getters and Setters here
}

We have to create the @Interface in the name of Mobile which actually does the mobile number validation with the help of MobileNumberValidator class. The configuration is below

package com.tipstocode.validator.custom;
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.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = MobileNumberValidator.class)
@Documented
public @interface Mobile {
  String message() default "Invalid Mobile Number. It should be 10 digit";
  Class[] groups() default { };
  Class[] payload() default { };
}

In the field message, we can put our own validation error message. Actually, this error message points to the properties of the validation message (ValidationMessage.properties).

@Constraint annotation determines which validator implementation class validates of mobile number with the help of @Mobile custom constraint or annotation.

Now we have to create a class called 'MobileNumberValidator' which implements 'ConstraintValidator' to validate the mobile number by using 'isValid' method. The configuration is below.

package com.tipstocode.validator.custom;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

class MobileNumberValidator implements ConstraintValidator<Mobile, String> {
@Override
public boolean isValid(String phoneNo, ConstraintValidatorContext context) {
if (phoneNo.matches("\\d{10}")) 
return true;
//validating phone number with -, . or spaces
else if(phoneNo.matches("\\d{3}[-\\.\\s]\\d{3}[-\\.\\s]\\d{4}")) 
return true;
//validating phone number with extension length from 3 to 5
else if(phoneNo.matches("\\d{3}-\\d{3}-\\d{4}\\s(x|(ext))\\d{3,5}")) 
return true;
//validating phone number where area code is in braces ()
else if(phoneNo.matches("\\(\\d{3}\\)-\\d{3}-\\d{4}"))
return true;
//return false if nothing matches the input
else return false;   
  }
}

That's it. Now the custom annotation @Mobile validates the mobile number perfectly. Now, let's start to create the custom annotation @ValidEmail to validate the Email ID.

Create Custom annotation to validate the Email ID

Here we have to follow the same steps which are used to create the custom annotation to validate the mobile number. So Lets directly go to the implementation here.

First, we have to create the custom annotation @Interface named as ValidEmail and create the custom validator class to validate the email ID. Both the snippets are below.

package com.tipstocode.validator.custom;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
@Documented
public @interface ValidEmail {
	  String message() default "Invalid Email ID";
	  Class<?>[] groups() default { };
	  Class<? extends Payload>[] payload() default { };
}
package com.tipstocode.validator.custom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class EmailValidator implements ConstraintValidator<ValidEmail, String>{
	
	public static final String VALID_EMAIL_REGEXP = "[-a-zA-Z0-9!#$%&'*+/=?^_'{|}~]+(?:\\.[-a-zA-Z0-9!#$%&'*+/=?^_'{|}~]+)*@([a-zA-Z0-9](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[-a-zA-Z0-9]*[a-zA-Z0-9])?)*(?:\\.[a-zA-Z0-9]{2,}+)|\\[(?:\\d{1,3}(?:\\.\\d{1,3}){3}|IPv6:[0-9A-Fa-f:]{4,39})\\])";	
	private static Pattern pattern;	
	private static Matcher matcher;	
	static {
		pattern = Pattern.compile(VALID_EMAIL_REGEXP);
	}		
	 @Override
	  public boolean isValid(String emailId, ConstraintValidatorContext context) {
		 matcher = pattern.matcher(emailId);
			return matcher.matches();  
	  }
}

Cool!. Now the custom annotation @ValidEmail validates the Email ID perfectly.

Sending Proper Validation Error Messages to Client

We have to send the validation error messages to the client in a proper way then only client-side implementation will be easy. Let start with how to do this.

Actually two kind of validation exception occurs ConstraintViolationException and MethodArgumentNotValidException. We have to catch the error messages from exceptions and attach that with our own format of response and return the same to the client.

Create a class called ValidationFailedResponse to collect the list of validation errors and Create a class called ViolationErrors to get and bind the validation errors.

package com.tipstocode.controller.controlleradvice;
import java.util.ArrayList;
import java.util.List;

public class ValidationFailedResponse {
  private List<ViolationErrors> violations = new ArrayList<>();
  public List<ViolationErrors> getViolations() {
    return violations;
  }
  public void setViolations(List<ViolationErrors> violations) {
    this.violations = violations;
  }
}

The below class is used to bind the error messages with corresponding fields.

package com.tipstocode.controller.controlleradvice;

public class ViolationErrors {
  private final String fieldName;
  private final String message;
  public ViolationErrors(String fieldName, String message) {
    this.fieldName = fieldName;
    this.message = message;
  }
  public String getFieldName() {
    return fieldName;
  }
  public String getMessage() {
    return message;
  }
}

Now let's create a ControllerAdvice class which is used to handle the exception to produce the structured way of response. This class applies globally to all the controllers.

The structure of ControllerAdvice class is below.

package com.tipstocode.controller.controlleradvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;

import org.springframework.http.HttpStatus;
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.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

@ControllerAdvice
class ControllerAdviceErrorHandling {
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    ValidationFailedResponse onConstraintValidationException(ConstraintViolationException e) {
        ValidationFailedResponse error = new ValidationFailedResponse();
        for (ConstraintViolation violation: e.getConstraintViolations()) {
            error.getViolations().add(new ViolationErrors(violation.getPropertyPath().toString(), violation.getMessage()));
        }
        return error;
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    ValidationFailedResponse onMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        ValidationFailedResponse error = new ValidationFailedResponse();
        for (FieldError fieldError: e.getBindingResult().getFieldErrors()) {
            error.getViolations().add(new ViolationErrors(fieldError.getField(), fieldError.getDefaultMessage()));
        }
        return error;
    }
}

The above class collects all the validations errors and processes with the structure of ValidationFailedResponse.

I hope you understood the concepts of Bean or Pojo Validation in Spring Boot.

Download the source code from GitHub

Leave a Reply

Your email address will not be published. Required fields are marked *