Search code examples
javaspringspring-bootconstraints

Spring Boot custom validation annotation not working as expected


I'm currently working on a Spring Boot project and trying to implement a custom validation annotation that should be able to validate both individual field of an object and lists of objects. However, my current implementation doesn't work and throwing an exception.

SampleValid class

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SampleValidator.class)
@Repeatable(SampleValid.List.class)
@Documented
public @interface SampleValid {

    String name() default "name";

    String message() default "";

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

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

    @Target({ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        SampleValid[] value();
    }
}

SampleValidator class

public class SampleValidator implements ConstraintValidator<SampleValid, Object> {
        private String fieldName;
    
        public void initialize(SampleValid constraintAnnotation) {
            name = constraintAnnotation.name();
        }
    
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            if (value == null) return true;
    
            try {
                String fieldValue = BeanUtils.getProperty(value, name);
    
                if (some validation goes here) {
                    context.disableDefaultConstraintViolation();
                    context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                            .addConstraintViolation();
                    return false;
                }
            } catch (Exception ex) {
                return false;
            }
    
            return true;
        }
    }

So when I am using this to validate a List it's throwing me an error, but this works with single objects,

@SampleValid
private List<SomeObject> someObjects;

@SampleValid
private SomeObject someObject; // this works fine

The error I am getting is,

java.lang.NoSuchMethodException: Unknown property 'name' on class 'class java.util.ArrayList'

Why isn't my current implementation working as expected?


Solution

  • The issue you're facing is because you are trying to access the "name" property of a List List<SomeObject> someObjects directly, but Lists don't have a "name" property.

    Your custom validation annotation and validator are designed to validate properties on individual objects, not on a List itself.

    If you want to validate properties of objects within the list, you should iterate through the list and validate each object individually.

    I'm thinking about a modification like this of your SampleValidator class:

    public class SampleValidator implements ConstraintValidator<SampleValid, Object> {
        private String fieldName;
    
        public void initialize(SampleValid constraintAnnotation) {
            fieldName = constraintAnnotation.fieldName();
        }
    
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            if (value == null) return true;
    
            if (value instanceof List) {
                List<?> list = (List<?>) value;
                for (Object item : list) {
                    if (item != null) {
                        try {
                            String className = item.getClass().getSimpleName();
                            String fieldValue = BeanUtils.getProperty(item, fieldName);
    
                            // Perform your validation here
                            if (some validation goes here) {
                                context.disableDefaultConstraintViolation();
                                context.buildConstraintViolationWithTemplate(className + context.getDefaultConstraintMessageTemplate())
                                        .addConstraintViolation();
                                return false;
                            }
                        } catch (Exception ex) {
                            return false;
                        }
                    }
                }
            }
    
            return true;
        }
    }
    

    in this case your validator will iterate through the list and validate each object's "name" property individually like this:

    @SampleValid
    private List<SomeObject> someObjects;