Saturday, May 28, 2022

 

Differences in @Valid and @Validated Annotations in Spring

1. Overview

In this quick tutorial, we'll focus on the differences between the @Valid and @Validated annotations in Spring.

Validating users' input is a common functionality in most of our applications. In the Java Ecosystem, we specifically use the Java Standard Bean Validation API to support this, which is well integrated with Spring from version 4.0 onward. The @Valid and @Validated annotations stem from this Standard Bean API.

In the next sections, we'll explore them in greater detail.

2. @Valid and @Validated Annotations

In Spring, we use JSR-303's @Valid annotation for method level validationWe also use it to mark a member attribute for validation. However, this annotation doesn't support group validation.

Groups help to limit the constraints applied during validation. One particular use case is UI wizards. In the first step, we may have a certain sub-group of fields. In the subsequent step, there may be another group belonging to the same bean. So we need to apply constraints on these limited fields in each step, but @Valid doesn't support this.

In this case, for group-level, we have to use Spring's @Validated, which is a variant of JSR-303's @Valid.  This is used at the method-level. For marking member attributes, we continue to use the @Valid annotation.

Now let's dive right in and look at the usage of these annotations with an example.

3. Example

Let's consider a simple user registration form developed using Spring Boot. To begin with, we'll have only the name and the password attributes:

public class UserAccount {

    @NotNull
    @Size(min = 4, max = 15)
    private String password;

    @NotBlank
    private String name;

    // standard constructors / setters / getters / toString
     
}

Next, let's look at the controller. Here we'll have the saveBasicInfo method with the @Valid annotation to validate the user input:

@RequestMapping(value = "/saveBasicInfo", method = RequestMethod.POST)
public String saveBasicInfo(
  @Valid @ModelAttribute("useraccount") UserAccount useraccount, 
  BindingResult result, 
  ModelMap model) {
    if (result.hasErrors()) {
        return "error";
    }
    return "success";
}

Now let's test this method:

@Test
public void givenSaveBasicInfo_whenCorrectInput_thenSuccess() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfo")
      .accept(MediaType.TEXT_HTML)
      .param("name", "test123")
      .param("password", "pass"))
      .andExpect(view().name("success"))
      .andExpect(status().isOk())
      .andDo(print());
}

After confirming that the test runs successfully, we'll extend the functionality. The next logical step is to convert this to a multi-step registration form, as is the case with most wizards. The first step with the name and password remains unchanged. In the second step, we'll fetch additional information like age and phone. Then we'll update our domain object with these additional fields:

public class UserAccount {
    
    @NotNull
    @Size(min = 4, max = 15)
    private String password;
 
    @NotBlank
    private String name;
 
    @Min(value = 18, message = "Age should not be less than 18")
    private int age;
 
    @NotBlank
    private String phone;
    
    // standard constructors / setters / getters / toString   
    
}

However, this time we'll notice that the previous test fails. This is because we aren't passing in the age and phone fields, which are still not in the picture on the UI. To support this behavior, we'll need group validation and the @Validated annotation.

For this, we need to group the fields creating two distinct groups. First, we'll need to create two marker interfaces, a separate one for each group or each step. We can refer to our article on group validation for the exact implementation of this. Here, let's focus on the differences in the annotations.

We'll have the BasicInfo interface for the first step, and the AdvanceInfo for the second step. Furthermore, we'll update our UserAccount class to use these marker interfaces:

public class UserAccount {
    
    @NotNull(groups = BasicInfo.class)
    @Size(min = 4, max = 15, groups = BasicInfo.class)
    private String password;
 
    @NotBlank(groups = BasicInfo.class)
    private String name;
 
    @Min(value = 18, message = "Age should not be less than 18", groups = AdvanceInfo.class)
    private int age;
 
    @NotBlank(groups = AdvanceInfo.class)
    private String phone;
    
    // standard constructors / setters / getters / toString   
    
}

In addition, we'll update our controller to use the @Validated annotation instead of @Valid:

@RequestMapping(value = "/saveBasicInfoStep1", method = RequestMethod.POST)
public String saveBasicInfoStep1(
  @Validated(BasicInfo.class) 
  @ModelAttribute("useraccount") UserAccount useraccount, 
  BindingResult result, ModelMap model) {
    if (result.hasErrors()) {
        return "error";
    }
    return "success";
}

As a result of this update, our test now runs successfully. We'll also test this new method:

@Test
public void givenSaveBasicInfoStep1_whenCorrectInput_thenSuccess() throws Exception {
    this.mockMvc.perform(MockMvcRequestBuilders.post("/saveBasicInfoStep1")
      .accept(MediaType.TEXT_HTML)
      .param("name", "test123")
      .param("password", "pass"))
      .andExpect(view().name("success"))
      .andExpect(status().isOk())
      .andDo(print());
}

This too runs successfully. Therefore, we can see how the usage of @Validated is essential for group validation.

Next, let's see how @Valid is essential to trigger the validation of nested attributes.

4. Using @Valid Annotation to Mark Nested Objects

The @Valid annotation is used to mark nested attributes, in particular. This triggers the validation of the nested object. For instance, in our current scenario, we can create a UserAddress object:

public class UserAddress {

    @NotBlank
    private String countryCode;

    // standard constructors / setters / getters / toString
}

To ensure validation of this nested object, we'll decorate the attribute with the @Valid annotation:

public class UserAccount {
    
    //...
    
    @Valid
    @NotNull(groups = AdvanceInfo.class)
    private UserAddress useraddress;
    
    // standard constructors / setters / getters / toString 
}

5. Pros and Cons

Let’s look at some of the pros and cons of using the @Valid and @Validated annotations in Spring.

The @Valid annotation ensures the validation of the whole object. Importantly, it performs the validation of the whole object graph. However, this creates issues for scenarios needing only partial validation.

On the other hand, we can use @Validated for group validation, including the above partial validation. However, in this instance, the validated entities have to know the validation rules for all the groups or use-cases they're used in. Here we're mixing concerns, which can result in an anti-pattern.

6. Conclusion

In this brief article, we explored the key differences between the @Valid and @Validated Annotations.

To conclude, for any basic validation, we'll use the JSR @Valid annotation in our method calls. On the other hand, for any group validation, including group sequences, we'll need to use Spring's @Validated annotation in our method call. The @Valid annotation is also needed to trigger the validation of nested properties.

As always, the code presented in this article is available over on GitHub.

No comments:

Post a Comment

So sánh các GitFlow model và áp dụng với CICD

https://medium.com/oho-software/so-s%C3%A1nh-c%C3%A1c-gitflow-model-v%C3%A0-%C3%A1p-d%E1%BB%A5ng-v%E1%BB%9Bi-cicd-b6581cfc893a