3

I am looking for annotation to annotate pojo class which I need to validate during request deserialization. I am searching for annotation to pass as parameter class which will validate my pojo.

Implementation can look like that:

@ValidateAnnotation(class = ExampleClassValidator.class)
public class ExampleClass {
    private String name;
}

Has anyone know any of spring annotation for that approach or some dependency which offer that declarative validation ? I am asking because I cannot find any similar solution in documentation.

Sebastian
  • 92
  • 2
  • 14
  • 1
    What kind of validation do you need? Does it have to be a custom validator or do you just standard validations like not null, max length, etc.? – clav Nov 25 '19 at 19:00
  • I need the custom validation which can be setup by annotate whole pojo class and passing validator as parameter. I heard that in spring is that annotation but I cannot find it. – Sebastian Nov 25 '19 at 19:05
  • 1
    Have a look at this link https://www.baeldung.com/javax-validation-method-constraints – Naveen Goyal Dec 02 '19 at 12:01
  • Unfortunately i am searching annotation for whole pojo to declare validator :/ – Sebastian Dec 02 '19 at 13:02

4 Answers4

1

You can use @InitBinder to configure a validator based on the target of the method. Here's a simple example:

Annotation class:

package test.xyz;


import org.springframework.validation.Validator;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface ValidateAnnotation {
    Class<? extends Validator> value();
}

The example class to be validated:

package test.xyz;

@ValidateAnnotation(ExampleClassValidator.class)
public class ExampleClass {
}

The validator class:

package test.xyz;

import org.springframework.validation.Errors;

public class ExampleClassValidator implements org.springframework.validation.Validator {
    @Override
    public boolean supports(Class<?> aClass) {
        return ExampleClass.class.isAssignableFrom(aClass);
    }

    @Override
    public void validate(Object o, Errors errors) {

    }
}

And finally the controller class with the @InitBinder definition:

import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import test.xyz.ExampleClass;
import test.xyz.ValidateAnnotation;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.Collections;

@Controller
public class ExampleController {
    @RequestMapping(value="test-endpoint", method= RequestMethod.GET)
    public @ResponseBody
    Object testMethod(@Valid ExampleClass exampleClass, Errors errors) {
        return Collections.singletonMap("success", true);
    }


    @InitBinder
    public void initBinder(WebDataBinder binder, HttpServletRequest request) throws IllegalAccessException, InstantiationException {
        Class<?> targetClass = binder.getTarget().getClass();
        if(targetClass.isAnnotationPresent(ValidateAnnotation.class)) {
            ValidateAnnotation annotation = targetClass.getAnnotation(ValidateAnnotation.class);
            Class<? extends Validator> value = annotation.value();
            Validator validator = value.newInstance();
            binder.setValidator(validator);
        }
    }
}

Explanation:

You can use the WebDataBinder's getTarget method to access the target to be validated. From there it is straightforward to check the annotation on the class, get the validator class, and set it on the binder. I believe you can also use the @ControllerAdvice annotation to configure a global InitBinder. As a disclaimer, I don't know if it is recommended to access the binder target within the InitBinder, but I haven't had any issues the few times I've done so.

heisbrandon
  • 1,180
  • 7
  • 8
  • Ye its pretty good solution, but I have almost the same. I cannot run away from that reflection to fetch property from annotation and i dont like it :/. But thank you for that important tip that global init binder can be configure in @ContorllerAdvice – Sebastian Dec 02 '19 at 20:05
0

For normal validation you can annotate your class with the annotations from the javax.validation.constraints package, like javax.validation.constraints.NotEmpty. For custom validation, you can make your own annotation that will call a custom validator that you write.

For example, if you wanted to create a validator that makes sure a field is nine characters long you could do the following:

First, create your custom validation annotation.

@Documented
@Constraint(validatedBy = NineCharactersValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface NineCharactersOnly {
    String message() default "This field must contain exactly nine characters";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Next, create your custom validator:

public class NineCharactersValidator implements ConstraintValidator<NineCharactersOnly, String> {

    @Override
    public void initialize(NineCharactersOnly contactNumber) {
    }

    @Override
    public boolean isValid(String contactField, ConstraintValidatorContext cxt) {
        return contactField != null && contactField.length() == 9;
    }
}

Next, use the annotation on fields that need to be constrained on your pojo.

public class ExampleClass {
    @NineCharactersOnly
    private String fieldThatMustBeNineCharacters;
}

Next, mark your method parameters in the controller with @Valid so they will be validated by Spring:

@RestController
public class CustomValidationController {

    @PostMapping("/customValidationPost")
    public ResponseEntity<String> customValidation(@Valid ExampleClass exampleClass, BindingResult result, Model m) {
        // we know the data is valid if we get this far because Spring automatically validates the input and 
        // throws a MethodArgumentNotValidException if it's invalid and returns an HTTP response of 400 (Bad Request).
        return ResponseEntity.ok("Data is valid");
    }   
}

Finally, if you want custom logic for handling validation errors instead of just sending a 400, you can create a custom validation handler method.

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationException(MethodArgumentNotValidException e) {
    Map<String, String> errors = new HashMap<>();
    d.getBindingResult().getAllErrors().forEach((error) -> {
        String fieldName = ((FieldError) error).getField();
        String errorMessage = error.getDefaultMessage();
        errors.put(fieldName, errorMessage);
    });

    return errors;
}
clav
  • 4,221
  • 30
  • 43
  • Yes i know that approach but I am searching annotation to annotate whole class and specify validator which will do all validation logic. I want to create simple Validator like this from org.springframework.validation.Validator and annotate whole class that should be validate by this class. – Sebastian Nov 26 '19 at 14:31
  • Have you seen this: https://stackoverflow.com/questions/39001106/implementing-custom-validation-logic-for-a-spring-boot-endpoint-using-a-combinat? The answer from Marco Blos appears to answer your question. – clav Nov 26 '19 at 18:50
  • Yes but this is still programmatically approach, i hope that i will find declarative approach to setup only annotation above the class which specific validator. – Sebastian Nov 26 '19 at 19:08
0

Maybe writing your custom annotation and using Spring AOP will help you. Spring AOP is quite simple.

Antal Attila
  • 588
  • 6
  • 18
  • Ye I though about it but i was sure that spring have that approach to define global validator for pojo with annotation – Sebastian Dec 02 '19 at 13:04
0

I found pretty good solution but in one place i used reflection :( Please feel free to comment and rate this solution, is it good enough or something could be done better.

  1. I had create own annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Validator {

    Class<? extends org.springframework.validation.Validator> validator();
}
  1. I next step I extend LocalValidatorFactoryBean to override validate method and here I was forced to use reflection to get class from annotation.
@Component
@RequiredArgsConstructor
class CustomLocalValidatorFactoryBean extends LocalValidatorFactoryBean {

    private final Map<Class<? extends Validator>, Validator> validators;

    @Override
    public void validate(Object target, Errors errors, Object... validationHints) {
        Class<? extends Validator> validatorKey = target.getClass().getAnnotation(com.validation.validator.Validator.class).validator();
        Optional.ofNullable(validators.get(validatorKey)).ifPresentOrElse(
                validator ->
                        validator.validate(target, errors),
                        () -> super.validate(target, errors, validationHints)
        );
    }
}
  1. I annotate pojo with my annotation to specify validator.
@Data
@Validator(validator = PersonValidator.class)
public class PersonDto {

    private final String name;
    private final String surname;
    private final Integer age;
}
  1. As you can see in my CustomLocalValidatorFactoryBean I injected a map of validators, in this map i store validators assigned to key which is the class of this validator. This class i specify in annotation in pojo to fetch suitable validator for currect validate target. And this is my configuration of validatos map.
@Configuration
class ValidatorConfig {

    @Bean
    Map<Class<? extends Validator>, Validator> validators() {
        var validators = new HashMap<Class<? extends Validator>, Validator>();
        validators.put(PersonValidator.class, new PersonValidator());
        return validators;
    }
}
  1. I specify custom @RestControllerAdvice and override method handleMethodArgumentNotValid.
@RestControllerAdvice
class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers,
                                                                  HttpStatus status, WebRequest request) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = error.getCode();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });

        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}
  1. And this is my validator, it could be bean with dao acces but it could also be simple pojo.
public class PersonValidator implements Validator {
    @Override
    public boolean supports(Class<?> aClass) {
        return PersonDto.class.isAssignableFrom(aClass);
    }

    @Override
    public void validate(Object object, Errors errors) {
        Optional.of(object).map(obj -> (PersonDto) obj).ifPresent(person -> {
            Optional.ofNullable(person.getName())
                    .filter(name -> Strings.isNotBlank(name) && name.length() >= 3)
                    .ifPresentOrElse(name -> doNothing(), () -> errors.reject("person.name", "name of person is invalid!"));
        });
    }
}

What do you think about that configuration, is it cannon on sparrow or you just like that solution ?

Sebastian
  • 92
  • 2
  • 14