2

I'm stuggling with getting only one error message per field. I have a lot of rules for each field and I want to validate them one by one. If one fails, validation stops and returns only one message describing failed rule for this field.

After research I've found something like @ReportAsSingleViolation annotation and it kinda works, but it have fixed message from custom constraint. So it's not what I want.

I've read about @GroupSequence but I can't get it working like I've described either.

This is my entity with custom constraint rules:

@Entity
@Table(name = "users", schema = "myschema")
public class User {
    private int id;

    @ValidLogin
    private String login;

    @ValidPassword
    private String password;

    @ValidEmail
    private String email;

    //getters & setters
}

And implementation of my custom constraint with couple built-in rules:

@Constraint(validatedBy = UsernameValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@NotEmpty
@Pattern(regexp = "^[a-zA-Z0-9]*$")
@Length.List({
        @Length(min = 3 , message = "{Length.min.user.login}"),
        @Length(max = 30, message = "{Length.max.user.login}")
})
public @interface ValidLogin {

    String message() default "";

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

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

And by default I get every message for failed rule in my jsp view. So again, I want to get it working like this: check for rule @NotEmpty and if it fails, return appropriate message, if not validate next rule @Pattern and so on.

Could you help? Thanks a lot!

3
  • Check this, it will help you: stackoverflow.com/questions/7555201/… Commented Aug 24, 2017 at 10:35
  • I mean add this annotation(@ReportAsSingleViolation) on your custom annotation Commented Aug 24, 2017 at 10:37
  • I've said I already tried it and why it's not what I want. It returns only general message defined for @ValidLogin, not for specific rule broken inside. Commented Aug 24, 2017 at 10:42

3 Answers 3

3

Here's what, I think, you are looking for:

@Test
public void test() {
    Validator v = Validation.byProvider( HibernateValidator.class )
            .configure()
            .buildValidatorFactory()
            .getValidator();

    // validations for each group - shows only corresponding violations even if other constraints
    // are violated as well
    assertThat( v.validate( new Bar( null, null ), First.class ) ).hasSize( 2 );
    assertThat( v.validate( new Bar( "", "" ), Second.class ) ).hasSize( 2 );
    assertThat( v.validate( new Bar( "a", "a" ), Third.class ) ).hasSize( 2 );

    // shows that validation will go group by group as defined in the sequence:
    //NotNull
    Set<ConstraintViolation<Bar>> violations = v.validate( new Bar( null, null ) );
    assertThat( violations ).hasSize( 2 );
    assertThat( violations ).extracting( "message" ).containsOnly( "must not be null" );

    //NotBlank
    violations = v.validate( new Bar( "", "" ) );
    assertThat( violations ).hasSize( 2 );
    assertThat( violations ).extracting( "message" ).containsOnly( "must not be blank" );

    //Size
    violations = v.validate( new Bar( "a", "a" ) );
    assertThat( violations ).hasSize( 2 );
    assertThat( violations ).extracting( "message" ).containsOnly( "size must be between 5 and 2147483647" );

}

@GroupSequence({ First.class, Second.class, Third.class, Bar.class })
private static class Bar {

    @NotNull(groups = First.class)
    @NotBlank(groups = Second.class)
    @Size(min = 5, groups = Third.class)
    private final String login;

    @NotNull(groups = First.class)
    @NotBlank(groups = Second.class)
    @Size(min = 5, groups = Third.class)
    private final String password;

    public Bar(String login, String password) {
        this.login = login;
        this.password = password;
    }
}

interface First {
}

interface Second {
}

interface Third {
}

I've added a test so it is visible how validation goes group by group. And to have such behavior you need to redefine a default group sequence for your bean. To do that you need to place @GroupSequence annotation on your bean that you'd like to validate, then list all the groups you need and don't forget to add the bean class itself (like in this example). Also all of this information is present here - in the documentation.


Edit

If you are OK with not using standard constraints you then might do something like:

    @Test
public void test2() throws Exception {
    Set<ConstraintViolation<Foo>> violations = validator.validate( new Foo( "", null ) );
    assertThat( violations ).hasSize( 2 );
    assertThat( violations ).extracting( "message" )
            .containsOnly( "value should be between 3 and 30 chars long", "Value cannot be null" );
}

private static class Foo {

    @ValidLogin
    private final String login;

    @ValidLogin
    private final String password;

    public Foo(String login, String password) {
        this.login = login;
        this.password = password;
    }
}

@Target({ FIELD })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { ValidLogin.ValidLoginValidator.class })
@interface ValidLogin {
    String message() default "message";

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

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

    class ValidLoginValidator implements ConstraintValidator<ValidLogin, String> {
        private static final Pattern PATTERN = Pattern.compile( "^[a-zA-Z0-9]*$" );

        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            String message = "";
            if ( value == null ) {
                message = "Value cannot be null";
            }
            else if ( !PATTERN.matcher( value ).matches() ) {
                message = "Value should match pattern ";
            }
            else if ( message.length() < 3 || message.length() > 30 ) {
                message = "value should be between 3 and 30 chars long";
            }
            if ( !message.isEmpty() ) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate( message ).addConstraintViolation();
            }
            return false;
        }
    }
}

In this case you just have your own custom constraint and validator for it. And you go check by check and then build the violation based on the first failed check. Also you could extract things like pattern and min, max as attributes to your constraint if you have similar checks to perform for login and password but for example with different patterns on string length...

Sign up to request clarification or add additional context in comments.

3 Comments

It's similar to answer before, what's the change? I've also said and showed that I'm applying built-in rules in custom constraint, not in a field. And again, this works like: for instance, if password is empty but login has lenght 3, there will be only one message for password that is empty. In that case there won't be message that login is too short, right?
@codeboy the difference is in the way where and how @GroupSequence is defined - this should solve the problem that you mentioned in your comment to previous solution with validation during persistence. And yeah ... this will check all for NotNull and if at least one is null then other fields even if they are null but not blank will not be checked for other constraints..
Yes, edit approach it's going to work, I've seen it before but I wanted to get it working with built-in constraints. Anyway, thank you for your effort.
0

I was wondering if GroupSequence didnt't work because of custom constraint and YES, that's why.

I was aplying built-in rules in custom constraint and there, groups for them don't work. In custom constraint was visible only one group DEFAULT, because of:

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

When I moved those built-in rules from custom constraint to field (which is more ugly now, I wanted to keep Entity pretty) this works.

But here we go again. Now it must go "level" by "level" meaning when one field is blank, but the others not, there be only one message for empty one. Others even thogh they are invalid, are waiting for next "level" sequence. Which is again - NOT WHAT I WANT.

Seems like getting one error per field is too much for spring / hibernate.

If anyone hava an idea how to get it working, please, let me know, I'll give it a try.

Comments

0

Each annotation has groups attribute that could be used for dividing checks to groups:

public class MyForm {

    @NotEmpty(groups = LoginLevel1.class)
    @Pattern(regexp = "^[a-zA-Z0-9]*$", groups = LoginLevel2.class)
    @Length.List({
        @Length(min = 3 , message = "{Length.min.user.login}", groups = LoginLevel3.class),
        @Length(max = 30, message = "{Length.max.user.login}", groups = LoginLevel3.class)
    })
    private String login;
}

Next step is to group these groups with @GroupSequence that allows to have fail-fast behavior:

public class MyForm {
    ...
    @GroupSequence({
        LoginLevel1.class,
        LoginLevel2.class,
        LoginLevel3.class
    })
    public interface LoginChecks {}

    public interface LoginLevel1 {}
    public interface LoginLevel2 {}
    public interface LoginLevel3 {}
}

The final step is to instruct Spring to validate using these group sequences:

@PostMapping("/form-handler")
public String processForm(@Validated({MyForm.LoginChecks.class, MyForm.PasswordChecks.class}) MyForm form, BindingResult result) {

    if (result.hasErrors()) {
        return null;
    }
    ...
}

4 Comments

@codeboy Please, try, test and ask questions. I've successfully using it in my project for some time already and I could miss something.
It doesn't work for me. I've tried it. I get following error: javax.validation.ConstraintViolationException: Validation failed for classes [package.entities.User] during persist time for groups [javax.validation.groups.Default, ] Seems like it still requires Default group, but when I put it at the end of my GroupSequence, again returns all errors at once. Maybe its due to custom constraint? In your example you puts rules annotations on field. PS: Defining multiple interfeces as public in one file is not allowed ^^
Thanks! I've updated my answer to put interfaces into the form class.
The error that you have means that you mix validation annotations and JPA annotations on the same object. Hibernate tries to validate the object before persisting it to the database and fails. You can disable it this additional validation or use different objects for validation and persistence.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.