Master Spring Boot Parameter Validation with Bean Validation and Hibernate Validator

This tutorial explains why API parameter validation is essential, introduces Java Bean Validation (JSR‑303/349/380) and its implementations, demonstrates using Spring's @Validated and @Valid annotations, shows Maven dependencies, sample DTO and controller code, and provides a unified exception‑handling strategy for clean error responses.

macrozheng
macrozheng
macrozheng
Master Spring Boot Parameter Validation with Bean Validation and Hibernate Validator

Overview

When providing reliable API interfaces, parameter validation is essential to ensure correct data storage. The article shows a typical menu‑creation validation method with many if‑else checks, illustrating the verbosity of manual validation.

/**
  * Validate parameters
  */
private void verifyForm(SysMenuEntity menu){
    if(StringUtils.isBlank(menu.getName())){
        throw new RRException("菜单名称不能为空");
    }
    if(menu.getParentId() == null){
        throw new RRException("上级菜单不能为空");
    }
    // menu type checks
    if(menu.getType() == Constant.MenuType.MENU.getValue()){
        if(StringUtils.isBlank(menu.getUrl())){
            throw new RRException("菜单URL不能为空");
        }
    }
    int parentType = Constant.MenuType.CATALOG.getValue();
    if(menu.getParentId() != 0){
        SysMenuEntity parentMenu = sysMenuService.getById(menu.getParentId());
        parentType = parentMenu.getType();
    }
    if(menu.getType() == Constant.MenuType.CATALOG.getValue() ||
       menu.getType() == Constant.MenuType.MENU.getValue()){
        if(parentType != Constant.MenuType.CATALOG.getValue()){
            throw new RRException("上级菜单只能为目录类型");
        }
        return;
    }
    if(menu.getType() == Constant.MenuType.BUTTON.getValue()){
        if(parentType != Constant.MenuType.MENU.getValue()){
            throw new RRException("上级菜单只能为菜单类型");
        }
        return;
    }
}

Parameter validation prevents malicious requests such as SQL injection. Java has the Bean Validation specification (JSR‑303/349/380) since 2009, now at version 2.0 (Jakarta Bean Validation 3.0 only changes package names).

Implementations include Hibernate Validator and Apache BVal. Although Hibernate is less popular in China, it provides many extensions such as Hibernate Search and OGM.

Spring Validation wraps Bean Validation; using @Validated on a controller enables declarative validation without calling the Bean Validation API directly.

Annotations

The javax.validation.constraints package defines 22 constraint annotations, grouped as follows:

Empty and non‑empty checks

@NotBlank

: string must not be null and trimmed length > 0 @NotEmpty: collection must not be empty; also works for strings @NotNull: must not be null @Null: must be null

Numeric checks

@DecimalMax(value)

: value must be less than or equal to the specified maximum @DecimalMin(value): value must be greater than or equal to the specified minimum @Digits(integer, fraction): numeric value must fit within the given integer and fraction limits @Positive: must be a positive number @PositiveOrZero: must be positive or zero @Max(value): value must be less than or equal to the given maximum @Min(value): value must be greater than or equal to the given minimum @Negative: must be a negative number @NegativeOrZero: must be negative or zero

Boolean checks

@AssertFalse

: annotated element must be false @AssertTrue: annotated element must be true

Length checks

@Size(min, max)

: validates size of strings, arrays, collections, maps, etc.

Date checks

@Future

: must be a future date @FutureOrPresent: must be present or future @Past: must be a past date @PastOrPresent: must be past or present

Other checks

@Email

: must be a valid email address @Pattern(value): must match the given regular expression

Hibernate Validator additional constraints

@Range(min=, max=)

: value must be within the specified range @Length(min=, max=): string length must be within the range @URL(protocol=,host=,port=,regexp=,flags=): must be a valid URL @SafeHtml: HTML content must be safe (no scripts, etc.)

@Valid and @Validated

@Valid

(from javax.validation) can be placed on methods, constructors, parameters, return values, or fields to trigger validation, supporting nested object validation. @Validated (from org.springframework.validation.annotation) is a Spring‑specific annotation that enables method‑level validation and supports validation groups.

Name

Spring Validation declarative

Nested validation support

Group validation support

@Validated

Yes

No

Yes

@Valid

No

Yes

No

In practice, use @Validated for most controller methods; use @Valid when you need nested object validation.

Quick Start

1. Add Maven dependencies

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>com.ratel</groupId>
    <artifactId>java-validation</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring AOP dependency -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
        </dependency>
        <!-- Lombok for convenience -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- Knife4j for API docs -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Spring Boot already brings in spring-boot-starter-validation, which includes hibernate-validator, so no extra dependency is required.

2. Create DTO

package com.ratel.validation.entity;

import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;

/**
  * @Description
  * @Date 2023/04/07
  * @Version 1.0
  */
@Data
public class UserAddDTO {
    /** Account */
    @NotEmpty(message = "登录账号不能为空")
    @Length(min = 5, max = 16, message = "账号长度为 5-16 位")
    @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
    private String username;

    /** Password */
    @NotEmpty(message = "密码不能为空")
    @Length(min = 4, max = 16, message = "密码长度为 4-16 位")
    private String password;
}

3. Create Controller

package com.ratel.validation.cotroller;

import com.ratel.validation.entity.UserAddDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;

@RestController
@RequestMapping("/users")
@Validated
public class UserController {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @GetMapping("/get")
    public UserAddDTO get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {
        logger.info("[get][id: {}]", id);
        UserAddDTO dto = new UserAddDTO();
        dto.setUsername("张三");
        dto.setPassword("123456");
        return dto;
    }

    @PostMapping("/add")
    public void add(@Valid @RequestBody UserAddDTO addDTO) {
        logger.info("[add][addDTO: {}]", addDTO);
    }
}

4. Test with Swagger

Start the application and open http://localhost:8080/doc.html#/home. Test the /users/get?id=-1 endpoint – the @Min constraint returns a 500 error (ConstraintViolationException). Test /users/add with an invalid JSON body such as {"password":"233","username":"33"}; the response is a 400 error with a JSON payload containing the validation messages for both fields.

{
  "timestamp":"2023-04-09T13:33:58.864+0000",
  "status":400,
  "error":"Bad Request",
  "errors":[
    {..."defaultMessage":"密码长度为 4-16 位"...},
    {..."defaultMessage":"账号长度为 5-16 位"...}
  ],
  "message":"Validation failed for object='userAddDTO'. Error count: 2",
  "path":"/users/add"
}

5. Unified Exception Handling

A GlobalExceptionHandler annotated with @ControllerAdvice captures MissingServletRequestParameterException, ConstraintViolationException, BindException, and MethodArgumentNotValidException, converting them into a standard CommonResult JSON structure. The handler also catches any other Exception and returns a generic system‑error response.

package com.ratel.validation.exception;

import com.ratel.validation.common.CommonResult;
import com.ratel.validation.enums.ServiceExceptionEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;

@ControllerAdvice(basePackages = "com.ratel.validation.cotroller")
public class GlobalExceptionHandler {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @ResponseBody
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public CommonResult missingServletRequestParameterExceptionHandler(HttpServletRequest req, MissingServletRequestParameterException ex) {
        logger.error("[missingServletRequestParameterExceptionHandler]", ex);
        return CommonResult.error(ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getCode(),
                                ServiceExceptionEnum.MISSING_REQUEST_PARAM_ERROR.getMessage());
    }

    @ResponseBody
    @ExceptionHandler(ConstraintViolationException.class)
    public CommonResult constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {
        logger.error("[constraintViolationExceptionHandler]", ex);
        StringBuilder detailMessage = new StringBuilder();
        for (ConstraintViolation<?> cv : ex.getConstraintViolations()) {
            if (detailMessage.length() > 0) detailMessage.append(";");
            detailMessage.append(cv.getMessage());
        }
        return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
                                ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
    }

    @ResponseBody
    @ExceptionHandler(BindException.class)
    public CommonResult bindExceptionHandler(HttpServletRequest req, BindException ex) {
        logger.error("[bindExceptionHandler]", ex);
        StringBuilder detailMessage = new StringBuilder();
        for (ObjectError oe : ex.getAllErrors()) {
            if (detailMessage.length() > 0) detailMessage.append(";");
            detailMessage.append(oe.getDefaultMessage());
        }
        return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
                                ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
    }

    @ResponseBody
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public CommonResult methodArgumentNotValidExceptionHandler(HttpServletRequest req, MethodArgumentNotValidException ex) {
        logger.error("[MethodArgumentNotValidException]", ex);
        StringBuilder detailMessage = new StringBuilder();
        for (ObjectError oe : ex.getBindingResult().getAllErrors()) {
            if (detailMessage.length() > 0) detailMessage.append(";");
            detailMessage.append(oe.getDefaultMessage());
        }
        return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),
                                ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
    }

    @ResponseBody
    @ExceptionHandler(Exception.class)
    public CommonResult exceptionHandler(HttpServletRequest req, Exception e) {
        logger.error("[exceptionHandler]", e);
        return CommonResult.error(ServiceExceptionEnum.SYS_ERROR.getCode(),
                                ServiceExceptionEnum.SYS_ERROR.getMessage());
    }
}

6. Result

After adding the global handler, validation errors are returned as concise strings (e.g., 账号长度为 5-16 位;密码长度为 4-16 位) instead of the verbose default JSON, improving readability for API consumers.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.