0

I have a custom validator that validates data against DB using repository:

@Constraint(validatedBy = DataValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomValidator {

    String message() default "some message";

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

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

@Component
public class DataValidator implements ConstraintValidator<CustomValidator, String> {

    @Autowired
    private DataRepository repository;

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        var data = repository.findDataByValue(value);
        //validation logic with result in 'isValid' variable
        return isValid;
    }
}

I have entity with a field that is annotated with DataValidator:

@Entity
@Table(name = "custom_data")
public class Data {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @DataValidator
    @NotBlank(message = "Value is mandatory")
    @Column
    private String value;

Spring Boot dependencies:

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

When I call repository.save(data) from the rest controller, my validator is called, but its repository field is null.

What configuration did I miss that DataRepository bean was injected to RestController correctly, but wasn't injected into DataValidator?

Dragon
  • 2,431
  • 11
  • 42
  • 68
  • Your questions are similar. This [answer](https://stackoverflow.com/a/17961063/10343888) will help you. – fatih May 07 '22 at 12:07
  • @Fati I tried it, still repository is null – Dragon May 07 '22 at 12:11
  • [This is your underlying problem](https://stackoverflow.com/questions/19896870/why-is-my-spring-autowired-field-null); Spring does not instantiate validator beans (the validator provider does), and autowiring isn't perfect. – chrylis -cautiouslyoptimistic- May 07 '22 at 12:12
  • @chrylis-cautiouslyoptimistic- hm, interesting, since I ran at many examples where simple Autowire is used and validator is annotated as Component. Perhaps those examples lost important details. – Dragon May 07 '22 at 12:20
  • All the beans have to be under Spring's control. No calls to new(). I'd also prefer constructor injection. Do you have a Configuration annotation on your main class? – duffymo May 07 '22 at 12:22
  • @duffymo yeah, and I don't use any new(). I just operate with Spring annotations. – Dragon May 07 '22 at 12:24

2 Answers2

0

Try it this way.

@Configuration
public class DataValidator implements ConstraintValidator<CustomValidator, String> {
    
    private static final DataValidator holder = new DataValidator();
    
    @Bean
    public static DataValidator bean(DataRepository repository) {
        holder.repository = repository;
        return holder;
    }

    private DataRepository repository;

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        var data = holder.repository.findDataByValue(value);
        //validation logic with result in 'isValid' variable
        return isValid;
     }
}
fatih
  • 1,285
  • 11
  • 27
0

I found a solution that will autowire the bean, but you need to call a validator manually.

First of all, add the following to application.properties to disable automatic validation trigger on data persistence:

spring.jpa.properties.javax.persistence.validation.mode=none

Create @Configuration class and describe Validator bean, configure validator factory for it:

@Bean
public Validator validator(AutowireCapableBeanFactory beanFactory) {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure()
            .constraintValidatorFactory(new SpringConstraintValidatorFactory(beanFactory))
            .buildValidatorFactory();

    return validatorFactory.getValidator();
}

Add validator to the class where you want to use it and and call its validate method directly:

@RestController
public class DataController {
    @Autowired
    private DataRepository repository;

    @Autowired
    private Validator validator;

    @PostMapping("/doSomething")
    public Data doSomething(@RequestBody Data data) {        
        var validationResult = validator.validate(data);
        //validation result processing
        return repository.save(data);
    }
}

or if you use validator within the REST endpoint as in this example, usage of the @Valid annotation is more correct, to my mind. Then you don't need to declare Validator bean:

@RestController
public class DataController {
    @Autowired
    private DataRepository repository;

    @PostMapping("/doSomething")
    public Data doSomething(@Valid @RequestBody Data data) {                
        return repository.save(data);
    }
}
Dragon
  • 2,431
  • 11
  • 42
  • 68