What are validation groups, and how do you apply different constraints for create vs update using @Validated?
answer
- group = empty marker interface
- constraint groups= attribute; default = Default
- @Validated(Group.class) triggers; @Valid can't
- Default not auto-included -> the trap
- @GroupSequence = ordered, stop at first failure
basics
~20 sValidation groups let one class hold constraints that apply only in certain scenarios. You tag a constraint with groups = Create.class, then trigger it with Spring's @Validated(Create.class) on the parameter. Plain @Valid can't select groups.
solid answer
~50 sA validation group is a marker interface used to scope constraints. Each constraint annotation has a `groups` attribute; assign it (`@NotNull(groups = Update.class)`) so the constraint only runs when that group is requested. To request a group you must use Spring's `@Validated({Update.class})` — the standard `@Valid` always uses the `Default` group and can't select others. Common pattern: `interface OnCreate {}` and `interface OnUpdate {}`; the id is `@Null` on create but `@NotNull` on update, while shared rules stay in the `Default` group. A constraint with no explicit group belongs to `Default`, so it won't run when you validate only `OnCreate` unless you include `Default` too or use `@GroupSequence`. Groups also enable ordered validation via `jakarta.validation.GroupSequence`, stopping at the first failing group. Overuse is a smell — separate DTOs are often clearer than heavy group juggling.
code
java · 21 linespublic interface OnCreate {}
public interface OnUpdate {}
public class ProductDto {
@Null(groups = OnCreate.class)
@NotNull(groups = OnUpdate.class)
private Long id;
@NotBlank(groups = {OnCreate.class, OnUpdate.class}) // in both
private String name;
}
@RestController
@RequestMapping("/products")
class ProductController {
@PostMapping
void create(@Validated(OnCreate.class) @RequestBody ProductDto dto) {}
@PutMapping("/{id}")
void update(@Validated(OnUpdate.class) @RequestBody ProductDto dto) {}
}go deeper
Know groups exist to reuse a class with scenario-specific constraints.
Wire a Create/Update example and know @Validated (not @Valid) selects the group.
Explain the Default-inclusion trap, @GroupSequence ordering, and when separate DTOs beat groups.
Set team conventions: DTO-per-use-case vs groups, cascade group conversion, and maintainability trade-offs.
## What a group is A **validation group** is just an empty **marker interface** (no methods). It labels a subset of constraints so you can run only that subset on demand. Every Bean Validation constraint has a `groups()` attribute defaulting to `{}`, which means the built-in `jakarta.validation.groups.Default` group. ```java public interface OnCreate {} public interface OnUpdate {} ``` ## Assigning constraints to groups ```java public class ProductDto { @Null(groups = OnCreate.class) // id must be absent on create @NotNull(groups = OnUpdate.class) // id required on update private Long id; @NotBlank // belongs to Default -> both flows if requested private String name; } ``` ## Triggering a group — must use @Validated The standard `@Valid` **cannot** name a group; it always validates the `Default` group. Spring's `@Validated` (`org.springframework.validation.annotation.Validated`) accepts group classes: ```java @PostMapping void create(@Validated(OnCreate.class) @RequestBody ProductDto dto) {} @PutMapping("/{id}") void update(@Validated(OnUpdate.class) @RequestBody ProductDto dto) {} ``` ## The Default-group trap A constraint with **no** `groups` belongs only to `Default`. If you validate with `@Validated(OnCreate.class)`, constraints in `Default` (like `@NotBlank name`) **do NOT run** unless you either: 1. Request both: `@Validated({OnCreate.class, Default.class})`, or 2. Make `OnCreate` extend `Default` (`interface OnCreate extends Default {}`), so requesting `OnCreate` includes `Default`, or 3. Explicitly put shared constraints in every group. This is the #1 gotcha — people forget shared constraints silently stop firing. ## Ordered validation with @GroupSequence `jakarta.validation.GroupSequence` defines an **ordered** sequence of groups; validation stops at the first group that produces violations. Useful to run cheap checks before expensive ones: ```java @GroupSequence({Basic.class, Expensive.class}) public interface FullChecks {} ``` ## Nesting / cascading Groups combine with `@Valid` on nested fields for cascade, but note that `@Valid` on a nested property does not translate group selection automatically — use `@ConvertGroup` if a nested object needs a different group. ## When to use vs. when to avoid - **Use** when the same payload class genuinely has state-dependent rules and duplicating a DTO would drift. - **Avoid / prefer separate DTOs** when create and update shapes diverge a lot; groups scattered across one class hurt readability and are easy to misconfigure (the Default trap). Many teams treat heavy group usage as a code smell. ## Summary of the mechanics 1. Group = marker interface. 2. Tag constraints via `groups = X.class`. 3. Trigger via `@Validated(X.class)` (never plain `@Valid`). 4. Remember `Default` isn't included automatically. 5. `@GroupSequence` for ordered, short-circuiting validation.
- You switch a create endpoint to @Validated(OnCreate.class) and suddenly your @NotBlank name check stops firing. Why?@NotBlank had no group, so it lives in the Default group, which isn't run when you request only OnCreate. Add Default to the parameter, put the constraint in OnCreate too, or make OnCreate extend Default.
- Why can't you select a group with plain @Valid?@Valid is the JSR standard trigger and always validates the Default group with no group argument. Group selection is Spring's extension exposed only through @Validated.
saying these in an interview costs you the question
- Thinking @Valid can take a group argument
- Assuming Default-group constraints always run alongside a requested group
- Believing groups require a custom validator class
- Treating @GroupSequence as running all groups rather than stopping at the first failure