skip to content

What are validation groups and how do you use them with Spring's @Validated?

level: seniorimportance: should knowfreq 40%

answer

  1. groups = empty marker interfaces
  2. constraint.groups() ; default = Default group
  3. @Validated(Group.class) selects groups; @Valid can't
  4. selecting a group DROPS Default unless included
  5. @GroupSequence = ordered, short-circuit

basics

~20 s

Validation groups let you apply different subsets of constraints in different situations. You tag constraints with a group marker interface, then activate that group — with Spring you pass the group class to @Validated(MyGroup.class) — so only those constraints run.

solid answer

~40 s

Validation groups partition constraints so you can validate different rules in different contexts — e.g., a fuller rule set on create vs. update. You define empty marker interfaces (e.g., interface OnCreate {}), then set groups on constraints: @NotNull(groups = OnCreate.class). By default constraints belong to jakarta.validation.groups.Default. To select groups programmatically you pass them to validator.validate(obj, OnCreate.class). With Spring method validation you put the target group on the class or method via org.springframework.validation.annotation.@Validated(OnCreate.class); Spring's @Validated (unlike jakarta @Valid) accepts a value() of group classes. Note that specifying a non-Default group means Default constraints are NOT run unless you include Default too or use @GroupSequence to order/inherit. Group sequences (@GroupSequence) let you enforce groups in order, short-circuiting after the first failing group. Common pitfall: forgetting that selecting one group drops the Default group.

code

java · 26 lines
java
import jakarta.validation.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.stereotype.Service;

interface OnCreate {}
interface OnUpdate {}

class User {
    @Null(groups = OnCreate.class)      // must be null when creating
    @NotNull(groups = OnUpdate.class)   // must be set when updating
    Long id;

    @NotBlank // no group -> belongs to Default group
    String email;
}

@Service
@Validated
class UserService {
    // method-level @Validated selects the active group
    @Validated(OnCreate.class)
    void create(User u) { /* only OnCreate constraints checked; Default NOT run here */ }

    @Validated(OnUpdate.class)
    void update(User u) { /* only OnUpdate constraints checked */ }
}

go deeper

for a junior

Know groups are marker interfaces that let you run different constraint subsets, activated via @Validated(Group.class).

for a middle

Explain the Default group and that selecting a group excludes Default; know @Validated vs @Valid for group support.

for a senior

Discuss group inheritance and @GroupSequence short-circuit ordering; method-level @Validated selecting groups.

for a principal

Weigh groups vs separate request types, redefining Default via @GroupSequence on the class, and multi-step validation design.

**Why groups exist:** The same object often needs **different validation rules in different situations**. Classic example: on **create**, `id` must be null and `email` required; on **update**, `id` must be present. Rather than two DTOs, you tag constraints with **groups** and activate the relevant group per operation. **Defining groups:** A group is just an **empty marker interface**: ```java public interface OnCreate {} public interface OnUpdate {} ``` No methods, no annotations required. **Assigning constraints to groups:** Every constraint annotation has a `groups()` member: ```java @Null(groups = OnCreate.class) @NotNull(groups = OnUpdate.class) private Long id; ``` A constraint with no explicit group belongs to the built-in **`jakarta.validation.groups.Default`** group. **Activating groups — programmatic:** ```java validator.validate(user, OnCreate.class); ``` Only constraints in `OnCreate` run. **Critical gotcha:** passing a specific group **excludes `Default`** unless you also pass `Default.class` or arrange inheritance. So `validate(user, OnCreate.class)` will **not** check unqualified constraints. **Activating groups — Spring method validation:** Spring's **`@Validated`** annotation has a `Class<?>[] value()` used as the active groups. Place it on the bean class or method: ```java @Validated(OnUpdate.class) public void update(@Valid User u) { ... } ``` Here `@Validated` at method level selects the group (this is the one meaningful method-level use of `@Validated`). The `@Valid` on the parameter cascades; the group filter comes from `@Validated`. This is also how MVC controllers pick groups for `@Validated @RequestBody`. **Group inheritance:** A group interface can `extends` another. If `interface Extended extends Default {}`, validating `Extended` also runs `Default` constraints — a common way to include defaults. **Group sequences (`@GroupSequence`):** Define an **ordered** sequence of groups; validation proceeds group by group and **stops at the first group with any violation** (short-circuit). Useful to run cheap checks before expensive ones: ```java @GroupSequence({Default.class, ExpensiveChecks.class}) public interface FullValidation {} ``` You can also put `@GroupSequence` on the entity class itself to redefine what `Default` means for that type, enabling ordered default validation. **Gotchas & best practice:** - **Selecting a non-Default group silently drops Default constraints** — the #1 surprise. Include `Default.class` explicitly or use inheritance. - Groups are **marker interfaces only**; putting logic in them is a smell. - Over-using groups on one mega-DTO can be harder to read than separate request types — weigh both. - `@Validated` (Spring) supports groups; `@Valid` (Jakarta) does **not** — that's a key reason to reach for `@Validated`. **When to use:** create-vs-update flows, multi-step wizards, tiered validation (basic vs. strict), and ordered/short-circuit validation via sequences.

  • If you validate with group OnCreate, do the unqualified @NotBlank (Default) constraints also run?
    No. Selecting a specific group excludes the Default group unless you also pass Default.class or make OnCreate extend Default. This is the most common groups gotcha.
  • Why can @Validated select groups but @Valid cannot?
    Spring's @Validated declares a Class<?>[] value() for group classes; the Jakarta @Valid annotation has no such attribute — it only marks cascade. So group selection requires @Validated.

saying these in an interview costs you the question

  • Assuming Default constraints still run when a specific group is selected
  • Thinking @Valid can specify groups
  • Putting methods/logic in group marker interfaces
  • Believing @GroupSequence runs all groups regardless of earlier failures

context