Search code examples
springspring-bootjakarta-validation

Why does @Size(min) validation trigger before @NotBlank validation in Spring Boot?


I'm facing an issue where both the @NotBlank and @Size(min) validations are triggered simultaneously on empty fields in Spring Boot, but I expect the @NotBlank validation to run first.

Here’s the scenario:

I have a field annotated with @NotBlank (to ensure it's not empty) and @Size(min = 2) (to ensure it's at least 2 characters). When I send an empty string in the request body, the validation triggers both errors: "Name is required" from @NotBlank "Name must be between 2 and 50 characters" from @Size(min = 2) However, I expected the validation to fail on @NotBlank first and not evaluate the @Size constraint when the field is empty.

Here’s the relevant part of my code:

@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;

Why is @Size(min = 2) being triggered even when the field is empty? How can I ensure that @NotBlank is evaluated first?

I’m using Spring Boot 3 with Jakarta Validation.

Any help would be much appreciated.


Solution

  • I have resolved this issue by creating a custom validator to enforce the validation order. Below is the solution I implemented:

    Custom Validator for @RequiredSize

    RequiredSize Annotation

    package com.example.app.model.validation;
    
    import jakarta.validation.Constraint;
    import jakarta.validation.Payload;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = RequiredSizeValidator.class)
    public @interface RequiredSize {
        String message() default "Invalid size";
        Class<?>[] groups() default {};
        Class<? extends Payload>[] payload() default {};
    
        int min() default 0;
        int max() default Integer.MAX_VALUE;
        String minMessage() default "Field must have at least {min} characters";
        String maxMessage() default "Field must have no more than {max} characters";
        String requiredMessage() default "This field is required";
    }
    

    RequiredSizeValidator

    package com.example.app.model.validation;
    
    import jakarta.validation.ConstraintValidator;
    import jakarta.validation.ConstraintValidatorContext;
    
    public class RequiredSizeValidator implements ConstraintValidator<RequiredSize, String> {
    
        private int min;
        private int max;
        private String minMessage;
        private String maxMessage;
        private String requiredMessage;
    
        @Override
        public void initialize(RequiredSize constraintAnnotation) {
            min = constraintAnnotation.min();
            max = constraintAnnotation.max();
            minMessage = constraintAnnotation.minMessage();
            maxMessage = constraintAnnotation.maxMessage();
            requiredMessage = constraintAnnotation.requiredMessage();
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            if (value == null || value.trim().isEmpty()) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(requiredMessage)
                       .addConstraintViolation();
                return false;
            }
    
            boolean valid = true;
    
            if (value.length() < min) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(minMessage)
                       .addConstraintViolation();
                valid = false;
            }
    
            if (value.length() > max) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(maxMessage)
                       .addConstraintViolation();
                valid = false;
            }
    
            return valid;
        }
    }
    

    Applying the Annotation

    package com.example.app.model;
    
    import jakarta.persistence.Entity;
    import jakarta.persistence.GeneratedValue;
    import jakarta.persistence.GenerationType;
    import jakarta.persistence.Id;
    import jakarta.validation.constraints.Email;
    import jakarta.validation.constraints.NotBlank;
    import jakarta.validation.constraints.Size;
    import jakarta.persistence.Table;
    import java.time.LocalDateTime;
    import com.example.app.model.validation.RequiredSize;
    
    @Entity
    @Table(name = "contact_requests")
    public class ContactRequest {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @RequiredSize(min = 2, max = 50, requiredMessage = "Name is required", minMessage = "Name must be at least 2 characters", maxMessage = "Name must be no more than 50 characters")
        private String name;
    
        @NotBlank(message = "Email is required")
        @Email(message = "Email should be valid")
        private String email;
    
        @NotBlank(message = "Message is required")
        @Size(max = 500, message = "Message cannot exceed 500 characters")
        private String message;
    
        private LocalDateTime submittedAt;
    
        // Getters and Setters
    }