Validation Groups
Sometimes the same DTO needs different rules in different scenarios. On create, the id must be absent; on update, it must be present. Validation groups solve this without duplicating the DTO — you tag constraints with marker interfaces and tell Spring which group to validate per endpoint using @Validated(Group.class).
The problem groups solve
Consider a single ProductRequest used for both creating and updating a product. The id should be null when creating (the server assigns it) but required when updating. With a single set of constraints you can’t express both rules at once. Groups let one DTO carry both.
Defining group interfaces
Groups are just empty marker interfaces — no methods, no implementation.
public interface OnCreate {}
public interface OnUpdate {}
A common convention is to nest them in a holder:
public final class ValidationGroups {
public interface OnCreate {}
public interface OnUpdate {}
private ValidationGroups() {}
}
Tagging constraints with groups
Every constraint annotation has a groups attribute. Assign each constraint to the scenario(s) where it applies. A constraint with no group belongs to the implicit Default group and runs only when Default is validated.
import jakarta.validation.constraints.*;
public record ProductRequest(
@Null(groups = OnCreate.class) // must be null on create
@NotNull(groups = OnUpdate.class) // must be present on update
Long id,
@NotBlank(groups = {OnCreate.class, OnUpdate.class})
String name,
@Positive(groups = {OnCreate.class, OnUpdate.class})
double price) {}
Selecting a group in the controller
This is where Spring’s @Validated matters — unlike @Valid, it accepts a group argument. Pass the group class to choose which constraints run.
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
public class ProductController {
@PostMapping
public Product create(
@Validated(OnCreate.class) @RequestBody ProductRequest request) {
return service.create(request);
}
@PutMapping("/{id}")
public Product update(@PathVariable Long id,
@Validated(OnUpdate.class) @RequestBody ProductRequest request) {
return service.update(id, request);
}
}
| Scenario | Annotation | id rule applied | Result for id != null |
|---|---|---|---|
| Create | @Validated(OnCreate.class) | @Null | rejected |
| Update | @Validated(OnUpdate.class) | @NotNull | accepted |
Warning: When you validate with a specific group, constraints in the
Defaultgroup are not run. Ifnameonly carried a plain@NotBlank(no group), it would be skipped on both endpoints. Either tag every relevant constraint with your groups, or have your group interface extendDefault(see below).
Including the Default group
To make a group also run the default (un-grouped) constraints, extend jakarta.validation.groups.Default:
import jakarta.validation.groups.Default;
public interface OnUpdate extends Default {}
Now @Validated(OnUpdate.class) validates both the OnUpdate-tagged constraints and every default constraint — a convenient way to layer extra rules on top of the baseline.
Group sequences — ordered validation
By default all constraints in the active group(s) run, even after one fails, producing multiple errors. A @GroupSequence runs groups in order and stops at the first group that has a violation — useful when later checks are expensive or only meaningful once earlier ones pass.
import jakarta.validation.GroupSequence;
import jakarta.validation.groups.Default;
public interface BasicChecks {}
public interface ExpensiveChecks {}
@GroupSequence({Default.class, BasicChecks.class, ExpensiveChecks.class})
public interface FullValidation {}
@PostMapping
public Account open(@Validated(FullValidation.class) @RequestBody AccountRequest req) {
return service.open(req);
}
With this sequence, the ExpensiveChecks group (say, a remote credit-check validator) only runs if Default and BasicChecks both passed. This short-circuits costly validation when cheap structural checks have already failed.
Tip: Reserve group sequences for genuinely ordered or expensive validation. For ordinary forms, returning all field errors at once gives the client a better experience than failing one at a time.
Groups on @ConfigurationProperties and method params
Groups work anywhere @Validated is used, including class-level method validation and validated configuration properties. The group argument selects the active constraint set in exactly the same way.