Backend Development 17 min read

Master Spring Boot Parameter Validation: Custom Rules, Group Checks, and Global Error Handling

This tutorial explains why parameter validation is essential in Spring Boot APIs, shows how to integrate the JSR‑303 Validator, demonstrates custom annotations, group validation, and simplifies error responses with a global exception handler, providing complete code examples for each step.

macrozheng
macrozheng
macrozheng
Master Spring Boot Parameter Validation: Custom Rules, Group Checks, and Global Error Handling

Today we discuss how to integrate the Validator parameter validation framework into Spring Boot and explore advanced techniques such as custom validation and group validation.

"This article assumes the code base from previous posts and that a global exception handler has already been added (repository link at the end)."

Parameter validation prevents illegal inputs from affecting business logic, replacing repetitive manual checks with declarative annotations like @NotBlank, @Email, and @Length.

"The Validator framework follows the JSR‑303 validation specification (Java Specification Requests)."

Why Parameter Validation Is Needed

In everyday API development, checking inputs such as non‑empty usernames, correct email formats, or phone numbers improves code readability and reduces boilerplate.

Integrating Validation in Spring Boot

Step 1: Add Dependencies

<code>&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-validation&lt;/artifactId&gt;
&lt;/dependency&gt;</code>
Note: Starting from springboot‑2.3 the validation library became a separate starter; earlier versions only required the web starter.

Step 2: Define the Entity to Validate

<code>@Data
public class ValidVO {
    private String id;

    @Length(min = 6, max = 12, message = "appId length must be between 6 and 12")
    private String appId;

    @NotBlank(message = "Name is required")
    private String name;

    @Email(message = "Please provide a valid email address")
    private String email;

    private String sex;

    @NotEmpty(message = "Level cannot be empty")
    private String level;
}</code>

Each field that needs validation should specify a user‑friendly message.

Common Constraint Annotations

@AssertFalse – must be false if not null

@AssertTrue – must be true if not null

@DecimalMax / @DecimalMin – numeric range limits

@Digits – restrict integer and fraction digits

@Future / @Past – date constraints

@Max / @Min – maximum and minimum values

@NotNull – cannot be null

@Null – must be null

@Pattern – must match a regex

@Size – collection/array size limits

@Email – email format

@Length – string length range

@NotBlank – non‑null and non‑empty after trim

@NotEmpty – collection/array not empty

@Range – value within a range

@URL – valid URL

Step 3: Create a Test Controller

<code>@RestController
@Slf4j
@Validated
public class ValidController {

    @ApiOperation("RequestBody validation")
    @PostMapping("/valid/test1")
    public String test1(@Validated @RequestBody ValidVO validVO) {
        log.info("validEntity is {}", validVO);
        return "test1 valid success";
    }

    @ApiOperation("Form validation")
    @PostMapping(value = "/valid/test2")
    public String test2(@Validated ValidVO validVO) {
        log.info("validEntity is {}", validVO);
        return "test2 valid success";
    }

    @ApiOperation("Single parameter validation")
    @PostMapping(value = "/valid/test3")
    public String test3(@Email String email) {
        log.info("email is {}", email);
        return "email valid success";
    }
}
</code>

Note that single‑parameter validation requires @Validated on the controller class.

Step 4: Observe Validation Errors

Calling

/valid/test1

with an invalid email returns

MethodArgumentNotValidException

.

Calling

/valid/test2

with missing name returns

BindException

.

Calling

/valid/test3

with an invalid email returns

ConstraintViolationException

.

<code>POST http://localhost:8080/valid/test1
Content-Type: application/json

{ "id":1, "level":"12", "email":"47693899", "appId":"ab1c" }</code>
<code>{"status":400,"message":"Name is required; Invalid email address; appId length must be between 6 and 12","data":null,"timestamp":1628435116680}</code>

Simplifying Global Exception Responses

The default Validator error payload is verbose. By customizing

RestExceptionHandler

we can intercept the three common validation exceptions and return a concise message.

<code>@ExceptionHandler({BindException.class, ValidationException.class, MethodArgumentNotValidException.class})
public ResponseEntity<ResultData<String>> handleValidatedException(Exception e) {
    ResultData<String> resp = null;
    if (e instanceof MethodArgumentNotValidException) {
        MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
        resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(),
                ex.getBindingResult().getAllErrors().stream()
                        .map(ObjectError::getDefaultMessage)
                        .collect(Collectors.joining("; ")));
    } else if (e instanceof ConstraintViolationException) {
        ConstraintViolationException ex = (ConstraintViolationException) e;
        resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(),
                ex.getConstraintViolations().stream()
                        .map(ConstraintViolation::getMessage)
                        .collect(Collectors.joining("; ")));
    } else if (e instanceof BindException) {
        BindException ex = (BindException) e;
        resp = ResultData.fail(HttpStatus.BAD_REQUEST.value(),
                ex.getAllErrors().stream()
                        .map(ObjectError::getDefaultMessage)
                        .collect(Collectors.joining("; ")));
    }
    return new ResponseEntity<>(resp, HttpStatus.BAD_REQUEST);
}
</code>
<code>{"status":400,"message":"Name is required; Invalid email address; appId length must be between 6 and 12","data":null,"timestamp":1628435116680}</code>

The response is now clean and user‑friendly.

Custom Parameter Validation

When built‑in annotations are insufficient, you can create your own.

Step 1: Define a Custom Annotation

<code>@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(EnumString.List.class)
@Documented
@Constraint(validatedBy = EnumStringValidator.class)
public @interface EnumString {
    String message() default "value not in enum values.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String[] value();
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
    @Retention(RUNTIME)
    @Documented
    @interface List { EnumString[] value(); }
}
</code>

Step 2: Implement the Validator Logic

<code>public class EnumStringValidator implements ConstraintValidator<EnumString, String> {
    private List<String> enumStringList;
    @Override
    public void initialize(EnumString constraintAnnotation) {
        enumStringList = Arrays.asList(constraintAnnotation.value());
    }
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return true;
        return enumStringList.contains(value);
    }
}
</code>

Step 3: Apply the Annotation

<code>@ApiModelProperty("Gender")
@EnumString(value = {"F", "M"}, message = "Gender must be F or M")
private String sex;
</code>
<code>POST http://localhost:8080/valid/test2
Content-Type: application/x-www-form-urlencoded

id=1&name=javadaily&level=12&[email protected]&appId=ab1cdddd&sex=N</code>
<code>{"status":400,"message":"Gender must be F or M","data":null,"timestamp":1628435243723}</code>

Group Validation

Different operations (create, update, query, delete) often require different mandatory fields. Spring’s group validation solves this without creating multiple VO classes.

Step 1: Define Group Interfaces

<code>public interface ValidGroup extends Default {
    interface Crud extends ValidGroup {}
    interface Create extends Crud {}
    interface Update extends Crud {}
    interface Query extends Crud {}
    interface Delete extends Crud {}
}
</code>

Step 2: Assign Groups to Model Fields

<code>@Null(groups = ValidGroup.Crud.Create.class)
@NotNull(groups = ValidGroup.Crud.Update.class, message = "Application ID cannot be null")
private String id;

@Null(groups = ValidGroup.Crud.Create.class)
@NotNull(groups = ValidGroup.Crud.Update.class, message = "Application ID cannot be null")
private String appId;

@NotBlank(groups = ValidGroup.Crud.Create.class, message = "Name is required")
private String name;

@Email(message = "Please provide a valid email address")
private String email;
</code>

Step 3: Use Groups in Controller Methods

<code>@PostMapping("/valid/add")
public String add(@Validated(value = ValidGroup.Crud.Create.class) ValidVO validVO) { ... }

@PostMapping("/valid/update")
public String update(@Validated(value = ValidGroup.Crud.Update.class) ValidVO validVO) { ... }
</code>
<code>POST http://localhost:8080/valid/add
Content-Type: application/x-www-form-urlencoded

name=javadaily&level=12&[email protected]&sex=F</code>

Creation succeeds because id and appId are optional; the same payload fails on update because those fields are required.

<code>{"status":400,"message":"ID cannot be null; Application ID cannot be null","data":null,"timestamp":1628492514313}</code>

Conclusion

Parameter validation is a high‑frequency requirement in real‑world development. Mastering custom annotations and group validation helps avoid boilerplate VO classes and produces cleaner, more maintainable APIs.

Exception HandlingValidationSpring BootCustom AnnotationGroup Validation
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

login 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.