Skip to content
Spring Boot sb validation 3 min read

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);
    }
}
ScenarioAnnotationid rule appliedResult for id != null
Create@Validated(OnCreate.class)@Nullrejected
Update@Validated(OnUpdate.class)@NotNullaccepted

Warning: When you validate with a specific group, constraints in the Default group are not run. If name only 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 extend Default (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.

Last updated June 13, 2026
Was this helpful?