Validation Exception - the right way to handle

1.6k Views Asked by At

I am trying to validate the postal code in my but the approach i am thinking of is not working out and I can't understand why.

I created a Validator, that hast to throw a ValidationException if it's not valid.

@Service
public class ZipCodeValidator{

    public void validate(String zipCode){
        validateNotEmpty(zipCode);
        validateHasNoSpaces(zipCode);
    }

    private void validateNotEmpty(String zipCode){
       if (zipCode.length() != 0){
           throw new ValidationException("Postal Code can't be empty");
       }
    }

    private void validateHasNoSpaces(String zipCode) {
        if (zipCode.contains(" ")){
            throw new ValidationException("Postal Code can't contain spaces");
        }
    }

}

In my service, where i receive the postal code I want to throw my custom exception (to which i pass the error message) like this:

    try{
        validator.validate(zipCode);
    }catch (ValidationException ex){
        throw new ZipCodeValidationException(ex.getMessage());
    }

However it doesn't seem to work, that exception is not caught so the program runs further.

What am I doing wrong?

Is the whole approach wrong? What would you recommend?

Here's my custom Exception

public class ZipCodeValidationException extends Exception{
    public ZipCodeValidationException(String message) {
        super(message);
    }
}
2

There are 2 best solutions below

0
On

I recommend the following:

  • to process all exceptions in universal place as ExceptionHandler class, for more details see: https://www.baeldung.com/exception-handling-for-rest-with-spring
  • you can extend ValidationException from RuntimeException, that approach will allow unloading the code from try-catch constructions
  • @Service annotation is not quite right for converters or validators, as rule @Service class contains business logic, for helpers classes will be better use @Component, in total no differences between these two annotations only understanding which layer of application that component has

Please share the code for more suggestions and help.

0
On

Hi Please find my answer in 2 steps, first the correct and then the second the suggested way to implement.

Correction:

Please use ObjectUtils.isEmpty(arg) for checking if string is 0 length or null. Here is the modified version of your code

public interface ZipcodeService {
        void validate(@Zipcode String zipCode) throws ZipCodeValidationException;
    }

    @Service
    public static class ZipcodeServiceImpl implements ZipcodeService {
        private final ZipCodeRegexMatcher zipCodeRegexMatcher;

        public ZipcodeServiceImpl() {
            zipCodeRegexMatcher = new ZipCodeRegexMatcher();
        }

        @Override
        public void validate(String zipCode) throws ZipCodeValidationException {
            // uncomment for Regex Validation
            // boolean valid = zipCodeRegexMatcher.isValid(zipCode);
            // uncomment for Simple text validation
            final boolean valid = !ObjectUtils.isEmpty(zipCode);

            if (!valid) {
                throw new ZipCodeValidationException("Invalid zipcode");
            }
        }
    }

This is how the caller looks like from Controller

@GetMapping(path = "dummy")
    public String getDummy(@RequestParam("zipcode") String zipCode) {
        try {
            zipcodeService.validate(zipCode);
            return zipCode;
        } catch (ZipCodeValidationException e) {
            return e.getMessage();
        }
    }

Suggested way:

add following entry to pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Create Annotation and Validator as given below

@Constraint(validatedBy = {ZipcodeValidator.class})
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Zipcode {
        String message() default "Invalid Zipcode value";

        Class<?>[] groups() default {};

        Class<? extends Payload>[] payload() default {};
    }

    public static class ZipcodeValidator implements ConstraintValidator<Zipcode, String> {
        private final ZipCodeRegexMatcher zipCodeRegexMatcher;

        public ZipcodeValidator() {
            zipCodeRegexMatcher = new ZipCodeRegexMatcher();
        }

        @Override
        public boolean isValid(String zipCode, ConstraintValidatorContext constraintValidatorContext) {
            return zipCodeRegexMatcher.isValid(zipCode);
        }
    }

Once this setup is done, head over to Controller class and annotated class with @Validated and field you want to have validation on with the Custom Annotation i.e Zipcode we have just created. We are creating a Custom Validator in this case ZipcodeValidator by extending ConstraintValidator.

This is how the caller looks like:

    @GetMapping
    public String get(@Zipcode @RequestParam("zipcode") String zipCode) {
        return zipCode;
    }

On Failed validation, it throws javax.validation.ConstraintViolationException: get.zipCode: Invalid Zipcode value which you can customize according to your need by using ControllerAdvice.

You can also use @Zipcode annotation at the service level and it works the same way. Regarding ZipCodeRegexMatcher instead of creating it inside the constructor you can create a bean and inject that dependency. It is a simple class that has regex for zipcode and performs validation.

public static class ZipCodeRegexMatcher {
        public static final String ZIP_REGEX = "^[0-9]{5}(?:-[0-9]{4})?$";
        private final Pattern pattern;

        public ZipCodeRegexMatcher() {
            pattern = Pattern.compile(ZIP_REGEX);
        }

        public boolean isValid(String zipCode) {
            return pattern.matcher(zipCode).matches();
        }
    }

The entire code is located here