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.

Comment From: philwebb

I believe that this is standard behavior for Jakarta Validation and not something that Spring Boot controls.

Comment From: alizainofficial25

@philwebb I understand this behavior is due to Jakarta Validation, but how can I ensure that NotBlank is validated before Size(min) in Spring Boot? Can you provide a solution or workaround to control this validation order?

Comment From: alizainofficial25

@philwebb I’m disappointed that my issue was closed without addressing my concern. The behavior where Size(min) is triggered before NotBlank seems incorrect and counterintuitive. I understand it’s related to Jakarta Validation, but can you clarify how I can ensure NotBlank is validated first in Spring Boot?

Closing the issue without providing a solution or workaround is frustrating, and I would appreciate further assistance on this matter.

Comment From: philwebb

@alizainofficial25, I appreciate that you're frustrated not finding an immediate solution, however, we're a small team and we need prioritize our time carefully. We're simply unable to provide support for all the libraries that we integrate with and as mentioned in the guidelines for contributing, we prefer to use GitHub issues only for bugs and enhancements.

A question like this is far better suited to stackoverflow.com where more of the community may be able to help and folks with more expertise with Jakarta Validation can get involved. From Spring's perspective, we delegate to Jakarta Validation and report the failures that it provides. If you don't want @Size to trigger when @NotBlank fails, that's really is a Jakarta Validation issue, and not one that we can help you with.

Comment From: alizainofficial25

@philwebb, Thank you for your response and clarification on the scope of support. I understand the constraints you face and appreciate your input.

In the meantime, 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
}

Comment From: alizainofficial25

@philwebb This custom implementation ensures that requiredMessage is triggered first when the field is empty, followed by minMessage and maxMessage if applicable.

I hope this helps others who may encounter the same issue. If any better approach becomes available in the future, I would be happy to adopt it.

Thank you again for your guidance!

Comment From: philwebb

I'm glad you solved the issue and thanks for sharing the solution.

Comment From: alizainofficial25

@philwebb You're very welcome! Thank you for your time.