using multiple validation on same attribute and stop on first failure

1k Views Asked by At

Java: 20 Springboot: 3.0.1

@NotBlank(message = "userId id can not be blank.")
@NotEmpty(message = "userId id can not be empty.")
@UUID
@User
private String userId;

now in request, I am not passing userId, I am getting any of the 4 validation error, but I am expecting, it should fail in the first validation(@NotBlank) itself.

I tried using GroupSequence like

@GroupSequence({Blank.class, Null.class, Empty.class, Custom.class, UserRequest.class})
  @UserType
  class UserRequest {
      @NotEmpty(groups = Empty.class, message = "userId id can not be empty.")
      @NotBlank(groups = Blank.class, message = "userId id can not be blank.")
      @NotNull(groups = Null.class, message = "userId id can not be null.")
      @UUID(groups = UID.class)
      @User(groups = Custom.class)
      private String userId;
  }

Still randomly error message is coming, it should first give Blank error message, then Null, then empty, then UUID, then custom.

2

There are 2 best solutions below

0
On

This is out of Spring Boot's control. It may be possible for Hibernate to order the reported constraint violations to match the order of the annotations but I'm not sure if the compiler, class file format, and reflection APIs provide any guarantees about that ordering. Also, jakarta.validation.Validator.validate(T, Class<?>...) returns a Set<ConstraintViolation<T>> which further suggests that there may be no guarantees about the order of the constraint violations.

From GitHub issue section: https://github.com/spring-projects/spring-boot/issues/35927

1
On

@GroupSequence should work according to Defining group sequences

By default, constraints are evaluated in no particular order, regardless of which groups they belong to. In some situations, however, it is useful to control the order in which constraints are evaluated.

One thing to note is the ordering should be (IMO):
NotNull -> NotEmpty -> NotBlank -> UUID As NotEmpty already checked NotNull and NotBlank already checked NotEmpty.

Below test is using spring boot 3.0.1 running with openjdk 20

import jakarta.validation.GroupSequence;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.hibernate.validator.constraints.UUID;

// validation is applied following this order
@GroupSequence({Null.class, Empty.class, Blank.class, UID.class, UserRequest.class})
public class UserRequest {
    // not related to annotation order here
    @NotBlank(groups = Blank.class, message = "userId id can not be blank.")
    @NotEmpty(groups = Empty.class, message = "userId id can not be empty.")
    @NotNull(groups = Null.class, message = "userId id can not be null.")
    @UUID(groups = UID.class)
    private String userId;

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }
}
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Set;
import java.util.stream.Stream;

@SpringBootTest
public class UserRequestValidateTest {

    @Autowired
    private Validator validator;

    @ParameterizedTest
    @MethodSource("invalidUserIds")
    void testUserValidationOrder(String userId, String expectedMessage) {
        UserRequest userRequest = new UserRequest();
        userRequest.setUserId(userId);
        Set<ConstraintViolation<UserRequest>> violations = validator.validate(userRequest, Null.class);
        assertTrue(violations.size() == 1);
        assertEquals(expectedMessage, violations.iterator().next().getMessage());
    }

    @MethodSource
    public static Stream<Arguments> invalidUserIds() {
        return Stream.of(arguments(null, "userId id can not be null."),
                arguments("", "userId id can not be empty."),
                arguments(" ", "userId id can not be blank."),
                arguments("not uuid", "must be a valid UUID"));
    }

}