1. Introduction

The Spring Validator interface provides a flexible and customizable way to validate objects. In this tutorial, we’ll explore how to use the Validator interface to validate objects in a Spring-based application.

2. Spring Validator Interface

The Validator interface is a part of the Spring Framework that provides a way to validate objects.

It’s a simple interface that defines two methods, supports() and validate(). These two methods are used to determine if the validator can validate an object and to perform the validation logic. 

2.1. supports(Class<?> clazz)

The supports() method in the Validator interface determines whether the validator can validate instances of a specific class**. This method takes in a parameter Class<?> clazz which represents the class of the object being validated**. It’s a generic class (Class<?>) to allow flexibility with different object types.

Specifically, Spring utilizes the isAssignableFrom() method to check if an object can be legally cast to an object of the validator’s supported class. Therefore, if the validator can handle objects of the provided clazz, it returns true, otherwise, it returns false to indicate that another validator should be used:

@Override
public boolean supports(Class<?> clazz) {
    return User.class.isAssignableFrom(clazz);
}

In this example, the validator is configured only to support validating objects of type User or its subclasses. The method isAssignableFrom() verifies compatibility through inheritance – it returns true for the User and its subclasses, and false for any other class type.

2.2. validate(Object target, Errors errors)

On the other hand, the validate() method plays a crucial role in Spring’s validation framework. It’s where we define the custom validation logic for the objects that the validator supports.

This method receives two key parameters:

  • Object target: This parameter represents the actual object to be validated. Spring MVC automatically passes the object we’re trying to validate to this method.
  • Errors: This parameter is an instance of the Errors interface. It provides various methods for adding validation errors to the object.

Here’s an example of a validate() method:

@Override
public void validate(Object target, Errors errors) {
    User user = (User) target;
    if (StringUtils.isEmpty(user.getName())) { 
        errors.rejectValue("name", "name.required", "Name cannot be empty"); 
    }
}

In this example, the validate() method performs various validations on the User object and adds specific error messages to the Errors object using rejectValue() for targeted field errors. Notably, rejectValue() takes three main parameters:

  • field: The name of the field that has an error, e.g., “name
  • errorCode: A unique code that identifies the error, e.g., “name.required
  • defaultMessage: A default error message that displays if no other message is found, e.g., “Name cannot be empty

3. Implementing a Validator

To create a validator, we need to implement the Validator interface. Here is an example of a simple validator that validates a User object:

public class UserValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return User.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        User user = (User) target;
        if (StringUtils.isEmpty(user.getName())) {
            errors.rejectValue("name", "name.required", "Name cannot be empty");
        }
        if (StringUtils.isEmpty(user.getEmail())) {
            errors.rejectValue("email", "email.required", "Invalid email format");
        }
    }
}

3.1. Creating the User Class

Before applying validation, it’s essential to define the structure of the object we intend to validate. Here’s an example of the User class:

public class User {
    private String name;
    private String email;

    // Getters and Setters
}

3.2. Configuring Spring Beans

Next, to integrate the custom validator into a Spring-based application, we can register it as a bean within our application context using a Spring configuration class. This registration ensures that the validator is available for dependency injection throughout the application lifecycle:

@Configuration
public class AppConfig implements WebMvcConfigurer{
    @Bean
    public UserValidator userValidator() {
        return new UserValidator();
    }
}

By annotating the userValidator() method with @Bean, we ensure it returns an object that Spring registers as a bean in the application context.

3.3. Integrating the Validator in a Spring MVC Controller

Once we register the validator, we can use it to validate a User object in a Spring MVC controller.

Next, let’s create a UserController to handle user-related requests:

@RestController
@RequestMapping("/api/users")
public class UserController {
    @Autowired
    private UserValidator userValidator;

    @PostMapping
    public ResponseEntity<?> createUser(@RequestBody User user) {
        Errors errors = new BeanPropertyBindingResult(user, "user");
        userValidator.validate(user, errors);
        if (errors.hasErrors()) {
            return ResponseEntity.badRequest().body(errors.getAllErrors());
        }
        // Save the user object to the database
        return ResponseEntity.ok("User created successfully!");
    }
}

In this example, we’re using Spring’s @RestController annotation to indicate that this controller returns JSON responses. Additionally, we use @RequestBody to bind the incoming JSON request body to a User object. If the validation fails, we return a 400 Bad Request response with a JSON body containing the error messages. Otherwise, we return a 200 OK response with a success message.

4. Testing With Curl

To test this API using curl, we can send a JSON request body with the User object data:

curl -X POST \
  http://localhost:8080/api/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"","email":""}'

This should return a 400 Bad Request response with a JSON body containing the error messages:

[
  {
    "codes": [
      "name.required.user.name",
      "name.required.name",
      "name.required.java.lang.String",
      "name.required"
    ],
    "arguments": null,
    "defaultMessage": "Name cannot be empty",
    "objectName": "user",
    "field": "name",
    "rejectedValue": "",
    "bindingFailure": false,
    "code": "name.required"
  },
  {
    "codes": [
      "email.required.user.email",
      "email.required.email",
      "email.required.java.lang.String",
      "email.required"
    ],
    "arguments": null,
    "defaultMessage": "Invalid email format",
    "objectName": "user",
    "field": "email",
    "rejectedValue": "",
    "bindingFailure": false,
    "code": "email.required"
  }
]

If we send a valid User object with a name and email, the API should return a 200 OK response along with a success message:

curl -X POST \
  http://localhost:8080/api/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"John Doe","email":"[email protected]"}'

As a result, the request returned a response with a success message:

"User created successfully!"

5. Validation Context

In addition, in some cases, we may want to pass additional context to the validator. Spring’s Validator interface supports validation context through the validate(Object target, Errors errors, Object… validationHints) method. Hence, to use validation context, we can pass additional objects as validation hints when calling the validate() method.

For example, we want to validate a User object based on a specific scenario:

public void validate(Object target, Errors errors, Object... validationHints) {
    User user = (User) target;
    if (validationHints.length > 0) {
        if (validationHints[0] == "create") {
            if (StringUtils.isEmpty(user.getName())) {
                errors.rejectValue("name", "name.required", "Name cannot be empty");
            }
            if (StringUtils.isEmpty(user.getEmail())) {
                errors.rejectValue("email", "email.required", "Invalid email format");
            }
        } else if (validationHints[0] == "update") {
            // Perform update-specific validation
            if (StringUtils.isEmpty(user.getName()) && StringUtils.isEmpty(user.getEmail())) {
                errors.rejectValue("name", "name.or.email.required", "Name or email cannot be empty");
            }
        }
    } else {
        // Perform default validation
    }
}

In this example, UserValidator checks the validationHints array to determine which validation scenario to use. Let’s update the UserController to use the UserValidator with validationHints:

@PutMapping("/{id}")
public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody User user) {
    Errors errors = new BeanPropertyBindingResult(user, "user");
    userValidator.validate(user, errors, "update");
    if (errors.hasErrors()) {
        return ResponseEntity.badRequest().body(errors.getAllErrors());
    }
    // Update the user object in the database
    return ResponseEntity.ok("User updated successfully!");
}

Now, let’s send the following curl command with both name and email fields empty:

curl -X PUT \
  http://localhost:8080/api/users/1 \
  -H 'Content-Type: application/json' \
  -d '{"name":"","email":""}'

The UserValidator returns a 400 Bad Request response with an error message:

[
  {
    "codes": [
      "name.or.email.required.user.name",
      "name.or.email.required.name",
      "name.or.email.required.java.lang.String",
      "name.or.email.required"
    ],
    "arguments": null,
    "defaultMessage": "Name or email cannot be empty",
    "objectName": "user",
    "field": "name",
    "rejectedValue": "",
    "bindingFailure": false,
    "code": "name.or.email.required"
  }
]

If we only pass one of the fields, for example, the name field, then the UserValidator allows the update to proceed:

curl -X PUT \
  http://localhost:8080/api/users/1 \
  -H 'Content-Type: application/json' \
  -d '{"name":"John Doe"}'

The response is a 200 OK response, indicating that the update was successful:

"User updated successfully!"

6. Conclusion

In this article, we’ve learned how to use the Validator interface to validate objects in a Spring-based application. We’ve explored the two methods of the Validator interface, supports() and validate(), and how to implement a custom validator to validate an object.

As always, the source code for the examples is available over on GitHub.