Mastering Elegant Parameter Validation in Spring Boot with Bean Validation

This article explains how to replace repetitive manual if‑else checks with Spring Boot’s built‑in Bean Validation, demonstrating the use of @Valid, @Validated, custom constraint annotations, group validation, and global exception handling to achieve clean, reusable, and maintainable parameter validation across controllers and services.

Alibaba Cloud Developer
Alibaba Cloud Developer
Alibaba Cloud Developer
Mastering Elegant Parameter Validation in Spring Boot with Bean Validation

Introduction

In everyday development, most methods need input parameter validation to ensure robustness. Manually writing many if‑else checks leads to redundant, hard‑to‑maintain code.

Problem Example

@RestController
public class TestController {
    private static final Pattern ID_CARD_PATTERN = Pattern.compile("(^\\d{15}$)|(^\\d{18}$)|(^\\d{17}(\\d|X|x)$)");
    private static final Pattern MOBILE_PHONE_PATTERN = Pattern.compile("^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$");

    @RequestMapping(value = "/api/saveUser", method = RequestMethod.POST)
    public Result<Boolean> saveUser(UserRequest user) {
        if (StringUtils.isBlank(user.getUserName())) {
            throw new IllegalArgumentException("用户姓名不能为空");
        }
        if (Objects.isNull(user.getGender())) {
            throw new IllegalArgumentException("性别不能为空");
        }
        if (Objects.isNull(GenderType.getGenderType(user.getGender()))) {
            throw new IllegalArgumentException("性别错误");
        }
        if (Objects.isNull(user.getAge())) {
            throw new IllegalArgumentException("年龄不能为空");
        }
        if (user.getAge() < 0 || user.getAge() > 150) {
            throw new IllegalArgumentException("年龄必须在0-150之间");
        }
        if (StringUtils.isBlank(user.getIdCard())) {
            throw new IllegalArgumentException("身份证号不能为空");
        }
        if (!ID_CARD_PATTERN.matcher(user.getIdCard()).find()) {
            throw new IllegalArgumentException("身份证号格式错误");
        }
        if (StringUtils.isBlank(user.getMobilePhone())) {
            throw new IllegalArgumentException("手机号不能为空");
        }
        if (!MOBILE_PHONE_PATTERN.matcher(user.getIdCard()).find()) {
            throw new IllegalArgumentException("手机号格式错误");
        }
        // 省略其他业务代码
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

This approach mixes validation with business logic, resulting in poor extensibility, readability, and reusability.

Solution with Spring Validation

Spring Boot provides built‑in Bean Validation (JSR‑380). By adding validation annotations to DTO fields and using @Valid or @Validated on controller methods, the framework automatically validates inputs.

Controller Parameter Validation Example

@Data
public class UserRequest {
    @NotBlank(message = "用户ID不能为空")
    private String userId;

    @NotBlank(message = "电话号码不能为空")
    @Pattern(regexp = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$", message = "电话号码格式错误")
    private String mobilePhone;

    @Min(message = "年龄必须大于0", value = 0)
    @Max(message = "年龄不能超过150", value = 150)
    private Integer age;

    @NotNull(message = "用户详情不能为空")
    @Valid
    private UserDetail userDetail;
    // 省略其他参数
}
@RestController
public class TestController {
    @RequestMapping(value = "/api/saveUser", method = RequestMethod.POST)
    public ResponseEntity<BaseResult> saveUser(@Validated @RequestBody UserRequest user) {
        // 省略业务代码
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

If validation fails, Spring throws MethodArgumentNotValidException, which by default returns a 400 error with a detailed JSON payload.

{
  "timestamp": 1666777674977,
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.web.bind.MethodArgumentNotValidException",
  "errors": [
    {
      "codes": ["NotBlank.UserRequest.mobilePhone", "NotBlank.mobilePhone", "NotBlank.java.lang.String", "NotBlank"],
      "defaultMessage": "电话号码不能为空",
      "objectName": "UserRequest",
      "field": "mobilePhone",
      "rejectedValue": null,
      "code": "NotBlank"
    }
  ],
  "message": "Validation failed for object='UserRequest'. Error count: 1",
  "path": "/api/saveUser"
}

To customize the error format, define a global exception handler:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        FieldError fieldError = e.getBindingResult().getFieldErrors().get(0);
        return new BaseResult(CommonResultCode.ILLEGAL_PARAMETERS.getErrorCode(),
                "入参中的" + fieldError.getField() + fieldError.getDefaultMessage(), EagleEye.getTraceId());
    }
}
{
  "success": false,
  "errorCode": "ILLEGAL_PARAMETERS",
  "errorMessage": "入参中的mobilePhone电话号码不能为空",
  "traceId": "1ef9749316674663696111017d73c9",
  "extInfo": {}
}

This yields a clean, uniform error response.

@Valid vs @Validated

@Valid is defined by Bean Validation and can be placed on methods, parameters, fields, and supports nested validation.

@Validated is a Spring annotation that adds group validation capability; it should be used on controller classes or methods when groups are needed.

Typical usage: use @Validated on the controller to trigger validation, and @Valid on nested objects.

Group Validation

Different scenarios may require different validation rules. Define groups and assign them to constraints:

@Data
public class UserRequest {
    @NotBlank(message = "用户ID不能为空", groups = {UpdateUser.class})
    private String userId;

    @NotBlank(message = "电话号码不能为空", groups = {UpdateUser.class, InsertUser.class})
    @Pattern(regexp = "^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d{8}$", message = "电话号码格式错误")
    private String mobilePhone;

    @Min(message = "年龄必须大于0", value = 0, groups = {UpdateUser.class, InsertUser.class})
    @Max(message = "年龄不能超过150", value = 150, groups = {UpdateUser.class, InsertUser.class})
    private Integer age;

    @NotNull(message = "用户详情不能为空", groups = {UpdateUser.class, InsertUser.class})
    @Valid
    private UserDetail userDetail;
    // 省略其他参数
}
@RestController
public class TestController {
    @RequestMapping(value = "/api/saveUser", method = RequestMethod.POST)
    public ResponseEntity<BaseResult> saveUser(@Validated(value = InsertUser.class) @RequestBody UserRequest user) {
        // 省略业务代码
        return new ResponseEntity<>(HttpStatus.OK);
    }
}

Custom Constraint Annotation

When built‑in annotations are insufficient, create a custom annotation and validator.

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = InEnumValidator.class)
public @interface InEnum {
    Class<? extends BasicEnum> enumType();
    String message() default "枚举类型不匹配";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
public class InEnumValidator implements ConstraintValidator<InEnum, Object> {
    private Class<? extends BasicEnum> enumType;
    @Override
    public void initialize(InEnum inEnum) { enumType = inEnum.enumType(); }
    @Override
    public boolean isValid(Object object, ConstraintValidatorContext ctx) {
        if (object == null) return true;
        if (enumType == null || !enumType.isEnum()) return false;
        for (BasicEnum e : enumType.getEnumConstants()) {
            if (e.getCode().equals(object)) return true;
        }
        return false;
    }
}
@Data
public class UserRequest {
    @InEnum(enumType = GenderEnum.class, message = "用户性别不在枚举范围内")
    private String gender;
    // 省略其他参数
}

Invalid inputs now produce a unified error response similar to the previous examples.

Validation in Service Layer

Apply validation to service interfaces and implementations using @Validated and @Valid.

public interface SchedulerServiceClient {
    List<JobConfigInfo> queryJobList(@NotBlank(message = "应用名称不能为空") String appName,
                                     @NotBlank(message = "环境不能为空") String env,
                                     Integer status,
                                     @NotBlank(message = "用户id不能为空") String userId);
}
@Component
@Validated
public class SchedulerServiceClientImpl implements SchedulerServiceClient {
    @Override
    public List<JobConfigInfo> queryJobList(String appName, String env, Integer status, String userId) {
        // 省略业务代码
    }
}

For object parameters, annotate fields with constraints and use @Valid on the interface method, then @Validated on the implementation.

@Data
public class CreateDingNotificationRequest {
    @NotNull(message = "通知类型不能为空")
    @InEnum(enumType = ProcessControlDingTypeEnum.class, message = "通知类型不在枚举值范围内")
    private String dingType;
    // 省略其他
}
public interface ProcessControlDingService {
    void createDingNotification(@Valid CreateDingNotificationRequest request);
}
@Component
@Validated
public class ProcessControlDingServiceImpl implements ProcessControlDingService {
    @Override
    public void createDingNotification(CreateDingNotificationRequest request) {
        // 省略业务代码
    }
}

FastValidatorUtils

A concise way to obtain all constraint violations:

Set<ConstraintViolation<T>> violationSet = FastValidatorUtils.validate(bean);

Custom @RequestValid annotation combined with an AOP aspect can also perform validation before method execution.

@Aspect
@Component
public class RequestValidAspect {
    @Around("@annotation(requestValid)")
    public Object around(ProceedingJoinPoint joinPoint, RequestValid requestValid) throws Throwable {
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg == null) continue;
            try {
                if (arg instanceof List && !((List<?>) arg).isEmpty()) {
                    for (Object item : (List<?>) arg) {
                        validate(item);
                    }
                } else {
                    validate(arg);
                }
            } catch (AlscBoltBizValidateException e) {
                // convert to desired format
            }
        }
        Object result = joinPoint.proceed();
        return result;
    }
    public static <T> void validate(T t) {
        Set<ConstraintViolation<T>> res = FastValidatorUtils.validate(t);
        if (!res.isEmpty()) {
            ConstraintViolation<T> cv = res.iterator().next();
            FastValidatorHelper.throwFastValidateException(cv);
        }
    }
}

Apply @RequestValid on methods to trigger this aspect.

Conclusion

By leveraging Spring’s Bean Validation, custom constraints, group validation, and global exception handling, developers can achieve clean, reusable, and maintainable parameter validation without cluttering business logic.

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.

Bean ValidationSpring BootParameter Validation
Alibaba Cloud Developer
Written by

Alibaba Cloud Developer

Alibaba's official tech channel, featuring all of its technology innovations.

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.