[Java] Batch handling of Spring validation errors with @ControllerAdvice

3 minute read

Conclusion

If you prepare a handler class with @ControllerAdvice , Handling of validation error of each controller can be processed in a batch.

Environment

Java 11 SpringBoot 2.3.3

Commentary

The REST controller that retrieves the user list by specifying the search parameters is explained below. Provide input validation of request and return 400 error together with the specified response body when validation error is detected.

  • Package and import statements are omitted.

Controller class

Add @Validated to the argument so that validation is performed. Handling at the time of error is not particularly done here.


@RestController
@RequiredArgsConstructor
public class UserController {

    @NonNull
    private final UserService userService;

    @NonNull
    private final GetUsersQueryValidator getUsersQueryValidator;

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.addValidators(getUsersQueryValidator);
    }

    /**
     * Specify search conditions and get user information
     *
     * @param getUsersQuery Search condition query parameter
     * @return User information retrieved
     */
    @GetMapping(value = "/users")
    ResponseEntity<List<UserDto>> getUsers(@Validated GetUsersQuery getUsersQuery) {

        SearchUsersCondition searchUsersCondition = new SearchUsersCondition();
        searchUsersCondition.setName(getUsersQuery.getName());
        searchUsersCondition.setLowerLimitAge(getUsersQuery.getLowerLimitAge());
        searchUsersCondition.setUpperLimitAge(getUsersQuery.getUpperLimitAge());

        return ResponseEntity.ok(userService.searchUsers(searchUsersCondition));
    }
}

Query parameter class

The class to which the request’s query parameters are bound. @NotBlank and @NotNull are assigned to each field so that unary validation is performed.

/**
 * Query parameter that specifies user search conditions
 */
@Data
public class GetUsersQuery {

    /**
     * username
     */
    @NotBlank
    private String name;

    /**
     * Minimum age
     */
    @NotNull
    private Integer lowerLimitAge;

    /**
     * Maximum age
     */
    @NotNull
    private Integer upperLimitAge;

}

Correlation validator class

An error occurs when the lower age limit of the query parameter exceeds the upper age limit.

/**
 * Correlation validator for {@link GetUsersQuery}
 */
@Component
public class GetUsersQueryValidator implements Validator {

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

    /**
     * Validation carried out
     *
     * @param target Validation target
     * @param errors Detected error
     */
    @Override
    public void validate(Object target, Errors errors) {

        // Correlation validation is not performed when unary error occurs in either upper limit age or lower limit age
        if (errors.hasFieldErrors("lowerLimitAge") || errors.hasFieldErrors("upperLimitAge")) {
            return;
        }

        GetUsersQuery getUsersQuery = GetUsersQuery.class.cast(target);

        int lowerLimitAge = getUsersQuery.getLowerLimitAge();
        int upperLimitAge = getUsersQuery.getUpperLimitAge();

        // If the upper limit age does not exceed the lower limit age, it is an error
        if (lowerLimitAge >= upperLimitAge) {
            errors.reject("reverseLimitAge");
        }
    }
}

Response body error class

When an error occurs, store this type of object in the response body.

/**
 * Error information set in the request body
 */
@Data
public class ApiError implements Serializable {

    private static final long serialVersionUID = 1L;

    private String message;
}

Exception handler class

Prepare an exception handler class with @ControllerAdvice as below.


/**
 * Handler of the exception that occurred in the controller
 */
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Autowired
    MessageSource messageSource;

    /**
     * Handle {@link BindException}
     *
     * @param bindException {@link BindException}
     * @param httpHeaders {@link HttpHeaders}
     * @param httpStatus {@link HttpStatus}
     * @param webRequest {@link WebRequest}
     * @return Response to client
     */
    @Override
    protected ResponseEntity<Object> handleBindException(
            BindException bindException,
            HttpHeaders httpHeaders,
            HttpStatus httpStatus,
            WebRequest webRequest
    ) {
        // Error list stored in the response body
        List<ApiError> apiErrorList = new ArrayList<>();

        List<ObjectError> objectErrorList = bindException.getAllErrors();

        for (ObjectError objectError :objectErrorList) {

            // get message from error code
            String message = messageSource.getMessage(objectError, webRequest.getLocale());

            // Create error object for response body and store in list
            ApiError apiError = new ApiError();
            apiError.setMessage(message);
            apiErrorList.add(apiError);
        }

        return new ResponseEntity<>(apiErrorList, httpHeaders, httpStatus);
    }
}

When a validation error occurs, a BindException containing error information is thrown from the controller. In the class to which @ControllerAdvice is assigned, implement the processing that you want to apply commonly to each controller. By inheriting ResponseEntityExceptionHandler and overriding the handleBindException method, the response at the time of validation error can be freely customized.

Here, it is customized as follows.

  • Specify response body as ApiError type.
  • Convert error code from objectError to error message.

The error code is stored in objectError in the following format.

Unary validation: “Annotation name + class name (camel case) + field name” Correlation validation: “Error code set by correlation validator + class name (camel case)”

If you prepare messages.properties as below, it will be converted to a message.

messages.properties

NotBlank.getUsersQuery.name=Enter name is required.


NotNull.getUsersQuery.lowerLimitAge=You must enter the lower age limit.
NotNull.getUsersQuery.upperLimitAge=You must enter the upper age limit.
reverseLimitAge.getUsersQuery=Please specify a value greater than the minimum age for the maximum age.