Mastering @Valid and @Validated: Practical Group Validation in Spring

This article explains the fundamental differences between Spring's @Valid and @Validated annotations, demonstrates how to apply group validation, nested object checks, and custom validation annotations, and provides a complete strategy for global exception handling and production‑grade best practices.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Mastering @Valid and @Validated: Practical Group Validation in Spring

1. Difference Between @Valid and @Validated

@Valid

is the original JSR‑303 annotation from the javax.validation package; it only supports basic field validation and recursive validation of nested objects. It cannot perform group validation and can be placed only on method parameters or fields. @Validated is Spring's extension in the org.springframework.validation package. It adds native support for validation groups, can be placed on controller classes or methods, but does not trigger recursive validation of nested objects unless those fields are also annotated with @Valid.

1.1 Origin and Capability

@Valid

belongs to the JSR‑303 standard, while @Validated belongs to Spring’s validation package. They serve different purposes and should not be mixed indiscriminately.

1.2 Recommended Usage in Production

Controller methods should declare @Validated(Group.class) to enable group validation, and any nested VO fields that require recursive checks must be annotated with @Valid. This combination is the standard practice in most projects.

1.3 Common Built‑in Validation Annotations

@NotBlank

: string must not be empty after trimming. @NotNull: object must not be null (suitable for numbers, dates, etc.). @NotEmpty: collection, array or string length must be greater than zero. @Min/@Max: numeric range limits. @Pattern: regex match (e.g., phone, ID, email). @Size: length range for strings or collections.

2. Basic Validation Without Groups

2.1 Adding Dependency

<!-- Spring Boot 2.x includes validation automatically -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- Spring Boot 3.x requires explicit Jakarta validation dependency -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2.2 Basic VO Definition

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.Data;

@Data
public class UserVO {
    @NotNull(message = "User ID cannot be null")
    private Long id;

    @NotBlank(message = "Username cannot be blank")
    private String username;

    @NotBlank(message = "Phone cannot be blank")
    private String phone;
}

2.3 Controller Validation Example

import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
@Validated
public class UserController {
    @PostMapping("/edit")
    public Result editUser(@RequestBody @Valid UserVO userVO) {
        // Business logic runs only after validation passes
        return Result.success();
    }
}

Using only @Valid shares a single rule set for all scenarios, which is suitable for simple, single‑purpose endpoints.

3. Group Validation Code Samples

3.1 Defining Group Marker Interfaces

// Add scenario
public interface AddGroup {}

// Update scenario
public interface UpdateGroup {}

3.2 VO with Group‑Specific Rules

import javax.validation.constraints.*;
import lombok.Data;

@Data
public class UserVO {
    // Only validated in the Update group
    @NotNull(message = "Update requires user ID", groups = UpdateGroup.class)
    private Long id;

    // Validated in both Add and Update groups
    @NotBlank(message = "Username cannot be blank", groups = {AddGroup.class, UpdateGroup.class})
    private String username;

    @NotBlank(message = "Phone cannot be blank", groups = {AddGroup.class, UpdateGroup.class})
    private String phone;
}

3.3 Controller Selecting a Group

@RestController
@RequestMapping("/user")
public class UserController {
    // Add endpoint: only AddGroup rules apply
    @PostMapping("/add")
    public Result addUser(@RequestBody @Validated(AddGroup.class) UserVO userVO) {
        return Result.success();
    }

    // Update endpoint: only UpdateGroup rules apply
    @PostMapping("/update")
    public Result updateUser(@RequestBody @Validated(UpdateGroup.class) UserVO userVO) {
        return Result.success();
    }
}

The add endpoint accepts a payload without id and passes validation, while the update endpoint rejects a missing id, cleanly separating business rules without duplicating VO classes.

3.4 Multiple Groups Simultaneously

When an interface needs both Add and Query rules, multiple group classes can be passed:

@PostMapping("/query")
public Result query(@RequestBody @Validated({AddGroup.class, QueryGroup.class}) UserVO vo) {
    return Result.success();
}

4. Nested Object Group Validation (Recursive VO Validation)

Spring’s @Validated alone does not recurse into nested objects. Adding @Valid on the nested field triggers recursive validation.

4.1 Child VO

@Data
public class AddressVO {
    @NotBlank(message = "Province cannot be blank", groups = {AddGroup.class, UpdateGroup.class})
    private String province;

    @NotBlank(message = "City cannot be blank", groups = {AddGroup.class, UpdateGroup.class})
    private String city;
}

4.2 Parent VO with Nested Object

@Data
public class UserVO {
    @NotNull(message = "Update requires ID", groups = UpdateGroup.class)
    private Long id;

    @NotBlank(message = "Username cannot be blank", groups = {AddGroup.class, UpdateGroup.class})
    private String username;

    // Enable recursive validation
    @Valid
    private AddressVO address;
}

4.3 Collection Nested Validation

Collections also require @Valid on the field to validate each element:

@Valid
@NotEmpty(message = "Address list cannot be empty", groups = AddGroup.class)
private List<AddressVO> addressList;

5. Global Exception Handling for Validation Errors

Validation failures throw MethodArgumentNotValidException for JSON bodies and BindException for GET/form parameters. A @RestControllerAdvice can capture both and return a unified error response.

import org.springframework.validation.*;
import org.springframework.web.bind.*;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestControllerAdvice
public class GlobalValidExceptionHandler {
    // JSON body validation errors
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().get(0).getDefaultMessage();
        return Result.fail(400, "Parameter validation failed: " + message);
    }

    // GET/form validation errors
    @ExceptionHandler(BindException.class)
    public Result<?> handleBindException(BindException e) {
        String message = e.getBindingResult().getFieldErrors().get(0).getDefaultMessage();
        return Result.fail(400, "Parameter validation failed: " + message);
    }
}

6. Custom Validation Annotation

When built‑in constraints cannot cover business rules (e.g., phone number format), a custom annotation with its validator can be created and used with groups.

6.1 Annotation Definition

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface PhoneValid {
    String message() default "Invalid phone format";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

6.2 Validator Implementation

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;

public class PhoneValidator implements ConstraintValidator<PhoneValid, String> {
    private static final String PHONE_REG = "^1[3-9]\\d{9}$";
    private Pattern pattern;

    @Override
    public void initialize(PhoneValid constraintAnnotation) {
        pattern = Pattern.compile(PHONE_REG);
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null || value.isEmpty()) {
            return true; // let @NotBlank handle emptiness if needed
        }
        return pattern.matcher(value).matches();
    }
}

6.3 Using Custom Annotation with Groups

@PhoneValid(groups = {AddGroup.class, UpdateGroup.class})
private String phone;

7. Common Pitfalls and Solutions

Group validation does not work when the controller only uses @Valid. Use @Validated(Group.class) on method parameters.

Nested object validation fails if @Valid is missing on the nested field. Add @Valid to every nested VO or collection field.

Id field is validated in the add scenario because its annotation lacks a group restriction. Bind the annotation to UpdateGroup only.

GET request validation errors are not caught because only MethodArgumentNotValidException is handled. Add a handler for BindException.

Custom annotation groups are ignored if the annotation does not declare a groups() attribute. Include the attribute with a default empty array.

Placing @Validated on the controller class without specifying a group causes all endpoints to share the same validation set, overriding method‑level groups. Keep class‑level @Validated without a group and specify groups on each method.

8. Production‑Grade Standardization

Combine @Validated(Group) on method parameters with @Valid on nested fields for full group and recursive validation.

Define separate group interfaces (AddGroup, UpdateGroup, QueryGroup) to let a single VO serve multiple scenarios.

Provide custom messages for every constraint; avoid default English messages.

Force @Valid on all nested VO and collection fields to prevent silent validation loss.

Implement a global exception handler that returns a unified error code and message for both JSON and form validation failures.

Encapsulate complex business rules in custom annotations to reduce repetitive if‑checks in service layers.

Eliminate manual null‑checks in services; rely on the validation layer to filter illegal input early.

9. Full Summary

@Valid

handles recursive validation, while @Validated provides group capabilities. Using them together forms the standard solution for Spring MVC parameter validation. Group validation enables a single VO to support add, edit, and query scenarios, dramatically reducing duplicated request objects and boilerplate null‑checks. Mastering basic constraints, nested validation, custom annotations, and a unified global exception handler yields a clean, maintainable, and interview‑ready validation architecture.

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.

JavaException HandlingSpringValidationCustom AnnotationGroup Validation
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

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.