Menu

#8 @Valid Annotation Support

1.0
open
nobody
None
2015-04-18
2015-04-15
globalbus
No

For example, I have a class like this

public class RegisterDto implements IRegisterDto {
    @Length(min = 2, max = 255)
    String alias;

    @Valid
    @NotNull
    EmailDto email;

    @Valid
    @NotNull
    Password password = new Password();

    @StringEnumeration(enumClass = Role.class)
    String type;

    String timeZone;
}

And nested class like this

public class Password implements IPassword {
    public Password(String password) {
        this.password=password;
    }

    public Password() {

    }

    @Length(min = 8, max = 255)
    @NotEmpty
    String password;
}

That example works perfectly with Spring MVC Form Binding. For nested classes, we need to have a constructor with String argument, and Property with the same name as field in form. There is probably more tricks like that, but they are not documented very well.

So, I want to have a validation for nested fields with @Valid annotation. I modified AnnotationExtractor for such functionality

    /**
     * Return annotations for a given field name
     */
    private List<Annotation> fieldAnnotations() {
        Field field = this.fieldFinder.findField(this.targetClass, this.targetFieldName);
        if (field != null) {
            List<Annotation> annotations = Arrays.asList(field.getAnnotations());
            List<Annotation> toAdd = new ArrayList<>();
            for(Annotation a:annotations)
                if(a.annotationType().isAssignableFrom(Valid.class)){
                    Field nestedField = this.fieldFinder.findField(field.getType(), this.targetFieldName);
                    if(nestedField!=null)
                        toAdd.addAll(Arrays.asList(nestedField.getAnnotations()));
                }
            toAdd.addAll(annotations);
            return toAdd;
        }
        return Collections.emptyList();
    }

It's not a perfect code, but working well in my case. There is probably better solution for somebody, who known code of this library better than me.

By the way, that project is a great time saving thing. Thanks for that!

Discussion

  • globalbus

    globalbus - 2015-04-16

    a little bit cleaner solution

        /**
         * Return annotations for a given field name
         */
        private List<Annotation> fieldAnnotations() {
            Field field = this.fieldFinder.findField(this.targetClass, this.targetFieldName);
            if (field != null) {
                List<Annotation> annotations = Arrays.asList(field.getAnnotations());
                List<Annotation> toAdd = new ArrayList<>(annotations);
                for(Annotation a:annotations)
                    if(a.annotationType().isAssignableFrom(Valid.class)){
                        toAdd.addAll(new AnnotationExtractor(field.getType()).getAnnotationsForField(this.targetFieldName));
                        break;
                    }
                return toAdd;
            }
            return Collections.emptyList();
        }
    
     
  • Francisco Perez Pellicena

    Hi,

    thanks for using the dialect, we appreciate your feedback.

    This library is focused on translate server validation to HTML5 client validation,
    so @Valid does not match this prerequisite.

    The annotation @Valid gives an indication to check the validity of the annotated field itself but, it doesn't define any validation rule unlike @Email for example, that establishes some sort of restrictions to the annotated field related to e-mail address formatting.

    In the example and the patch that you provided as a solution, there're some conditions not generally applicable.
    - You're searching a @Valid annotation, but this should not be the general rule.
    - You're using the same identifier "password" in both, the parent class field as in the class field and these could be different.

    Well, the good news.
    Html5ValidationDialect supports nested classes, so your Password class should be validated as well.
    The point here underlies in the HTML form and the validation object, RegisterDto.
    In order to validate the password field, given your classes structure, your HTML should look like:

    <form method="post" th:action="@{/saveForm.html}" th:object="${registerDto}">
        <!-- ... -->
        <input type="type" th:field="*{type}" />
        <input type="password" th:field="*{password.password}" />
        <!-- and so on ...-->
    </form>
    

    This should work. Ensure the th:field attribute for the password field references the final property in the field chain.
    You can find in the AnnotationExtractorTest unit test a similar use case.
    As an exercise, you can write your own test to check that could look like:

    @Test
    public void regressionTest() {
        AnnotationExtractor extractor = AnnotationExtractor.forClass(RegisterDto.class);
        List<Annotation> annotations = extractor.getAnnotationsForField("alias");
        assertEquals(1, annotations.size());
        Class lengthClass = annotations.get(0).getClass();
        assertTrue(Length.class.isAssignableFrom(lengthClass));
        // Field
        annotations = extractor.getAnnotationsForField("password");
        assertEquals(2, annotations.size());
        Class validClass = annotations.get(0).getClass();
        assertTrue(Valid.class.isAssignableFrom(validClass));
        Class notNullClass = annotations.get(1).getClass();
        assertTrue(NotNull.class.isAssignableFrom(notNullClass));
        // Nested
        annotations = extractor.getAnnotationsForField("password.password");
        assertEquals(2, annotations.size());
        lengthClass = annotations.get(0).getClass();
        assertTrue(Length.class.isAssignableFrom(lengthClass));
        Class notEmptyClass = annotations.get(1).getClass();
        assertTrue(NotEmpty.class.isAssignableFrom(notEmptyClass));
    }
    

    Final thoughts.
    - A validation class should contain plain fields to maintain the HTML code as simple as possible.
    - So, a good rule of thumb is, layer isolation. Don't pass domain model classes to the view layer. Transform them before sending to the view layer and build domain classes after posting forms. Changes in domain classes model should not induce changes in form classes model.
    - In your case, you could simply write the following form classes model. Notice the getter to build the Password instance.

    public class RegisterDto implements IRegisterDto {
        @Length(min = 2, max = 255)
        String alias;
    
        @Valid
        @NotNull
        EmailDto email;
    
        @Length(min = 8, max = 255)
        @NotEmpty
        String password;
    
        @StringEnumeration(enumClass = Role.class)
        String type;
    
        String timeZone;
    
        public Password getPassword() {
        return new Password(this.password);
        }
    }
    
     
  • globalbus

    globalbus - 2015-04-18

    Yep, I know about the @Valid is special case of JSR303 Annotation. That's why I cannot provide solution by implementing custom ValidationPerformer.

    • You're searching a @Valid annotation, but this should not be the general rule.

    That is a default meaning of @Valid, right?

    • You're using the same identifier "password" in both, the parent class field as in the class field and these could be different.

    Yep, that is true. I will probably need to find all annotations nested field.

    This should work. Ensure the th:field attribute for the password field references the final property in the field chain.

    That's a pretty elegant solution. Only a little more chance to have a typo.

    • A validation class should contain plain fields to maintain the HTML code as simple as possible.

    I'm doing nested fields to share validation rules between many forms.

    • So, a good rule of thumb is, layer isolation. Don't pass domain model classes to the view layer.

    I already have a 2 layers. That's a data transfer object, with minimum logic (only custom conversions between types).

    Now I'm doing much more experiments in that library. In example, I need to have a possibility to override default ValidationPerformers behavior, so I changed ValidationPerformerFactory.performers to Map.

    I will probably post better solution, for such problem, soon.

     

    Last edit: globalbus 2015-04-18

Log in to post a comment.