[JAVA] Collective handling of Spring validation errors with @ControllerAdvice

Conclusion

If you prepare a handler class with @ControllerAdvice , Handling of validation error of each controller can be handled at once.

environment

Java 11 SpringBoot 2.3.3

Commentary

The following is an explanation of the REST controller that acquires a list of users by specifying search parameters. Input validation of the request is provided, and when a validation error is detected, 400 error is returned together with the specified response body.

Controller class

Add @Validated to the argument so that validation is performed. Handling at the time of error is not particularly performed 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);
    }

    /**
     *Get user information by specifying search conditions
     *
     * @param getUsersQuery Search Criteria Query Parameters
     * @return Searched user information
     */
    @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 query parameters of the request are bound. @NotBlank and @NotNull are added to each field so that unary validation is performed.

/**
 *Query parameters that specify user search criteria
 */
@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 limit age of the query parameter exceeds the upper limit age.

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

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

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

        //No correlation validation is performed if a unary error occurs in either the upper age limit or the 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 age limit does not exceed the lower age limit, an error will occur.
        if (lowerLimitAge >= upperLimitAge) {
            errors.reject("reverseLimitAge");
        }
    }
}

Error class for response body

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

/**
 *Error information to be 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 shown below.


/**
 *Handler for exceptions raised on the controller
 */
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Autowired
    MessageSource messageSource;

    /**
     * {@link BindException}Handling
     *
     * @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 an error object for the response body and store it in the list
            ApiError apiError = new ApiError();
            apiError.setMessage(message);
            apiErrorList.add(apiError);
        }

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

When a validation error occurs, the controller throws a BindException that stores the error information. In the class with @ControllerAdvice, implement the process that you want to apply to each controller. By inheriting ResponseEntityExceptionHandler and overriding the handleBindException method, you can freely customize the response at the time of validation error.

Here, it is customized as follows.

--Specify the response body as ʻApiError type. --Converted error code of ʻ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 follows, it will be converted into a message.

messages.properties

NotBlank.getUsersQuery.name=Entering a name is mandatory.
NotNull.getUsersQuery.lowerLimitAge=Entering the minimum age is mandatory.
NotNull.getUsersQuery.upperLimitAge=Entering the maximum age is mandatory.
reverseLimitAge.getUsersQuery=Specify a value larger than the lower limit age for the upper limit age.

Recommended Posts

Collective handling of Spring validation errors with @ControllerAdvice
Self-made Validation with Spring
Get validation results with Spring Boot
Form class validation test with Spring Boot
Get Body part of HttpResponse with Filter of Spring
[Java] Article to add validation with Spring Boot 2.3.1.
I need validation of Spring Data for Pageable ~
Customization of validation
Compatibility of Spring JDBC and MyBatis with Spring Data JDBC (provisional)
Access the built-in h2db of spring boot with jdbcTemplate
Create Restapi with Spring Boot ((1) Until Run of App)
How to boot by environment with Spring Boot of Maven
Control the processing flow of Spring Batch with JavaConfig.