Ali Zain
Ali Zain

Reputation: 21

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.

Upvotes: 1

Views: 49

Answers (2)

Ali Zain
Ali Zain

Reputation: 21

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
}

Upvotes: 0

Vedant Kakade
Vedant Kakade

Reputation: 41

This is actually a common issue when working with validation annotations in Spring Boot. The problem here is that both @NotBlank and @Size(min = 2) are getting triggered, even when you send an empty string. Here’s why:

@NotBlank checks if the string is not empty (or just whitespace). @Size(min = 2) checks the length of the string to make sure it has at least 2 characters. When you send an empty string, @NotBlank will fail first because the field is empty. But Spring’s validation process doesn’t stop there — it will still evaluate other constraints, like @Size(min = 2), and give you both error messages.

In terms of validation flow, Spring doesn’t guarantee that @NotBlank will always run first, and both annotations are usually evaluated together. So in this case, you’re seeing both errors even though you expected @NotBlank to take precedence.

Upvotes: 0

Related Questions