skip to content

How does @Validated enable method-level validation on Spring beans, and what makes it work?

level: middleimportance: must knowfreq 65%

answer

  1. @Validated on the TYPE, not the method
  2. MethodValidationPostProcessor -> AOP proxy
  3. throws ConstraintViolationException (jakarta), not MethodArgumentNotValid
  4. self-invocation bypasses it
  5. @Valid param = cascade into graph

basics

~20 s

Put @Validated on a bean's class; then constraint annotations on its method parameters (like @NotNull) and return values are enforced on every call. A MethodValidationPostProcessor creates an AOP proxy that checks them and throws ConstraintViolationException on failure.

solid answer

~40 s

@Validated (org.springframework.validation.annotation.Validated) at the class level marks a Spring bean for method validation. A MethodValidationPostProcessor bean-post-processor detects it and wraps the bean in an AOP proxy backed by a MethodValidationInterceptor. On each method call the interceptor validates parameters annotated with constraints (@NotNull, @Size, etc.) and, if the return value is annotated, validates that too; cascade with @Valid on a parameter to validate nested objects. On failure it throws jakarta.validation.ConstraintViolationException carrying the violations — this happens before/after the method body respectively. Spring Boot auto-configures MethodValidationPostProcessor. Because it is proxy-based, the usual AOP caveats apply: only external calls through the proxy are intercepted (self-invocation is not), the bean must be a Spring-managed bean, and the target should be proxyable. This is distinct from @Valid request-body binding in MVC.

code

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

@Service
@Validated // <-- class level: enables method validation via the post-processor's proxy
class PricingService {

    // param constraints enforced on every external call
    @NotNull
    public Money quote(@NotBlank String sku, @Positive int qty) {
        return Money.of(qty * 100);
    }

    // @Valid cascades into the nested object's own constraints
    public void submit(@Valid Order order) { /* ... */ }
}
// Calling quote("", 0) throws jakarta.validation.ConstraintViolationException BEFORE the body runs.

go deeper

for a junior

Know that @Validated on the class plus constraint annotations on parameters enforces them on calls.

for a middle

Explain the two parts (annotation + MethodValidationPostProcessor), the proxy, and that it throws ConstraintViolationException.

for a senior

Discuss AOP proxy caveats (self-invocation, proxyability), return-value validation, and @Validated vs @Valid roles.

for a principal

Contrast with Spring 6.1 built-in controller method validation, discuss where to place the boundary, and exception-translation strategy across the app.

**The problem it solves:** Field-level constraints only fire when something calls `validator.validate(obj)`. **Method validation** (part of Bean Validation 1.1+/Jakarta Validation) lets you enforce constraints on **method parameters and return values** automatically whenever the method is invoked — so a service method can declare `save(@NotNull @Valid Order o)` and callers can't sneak past. **Two moving parts:** 1. **`@org.springframework.validation.annotation.Validated`** placed on the **class** (type level) marks the bean as a candidate for method validation. (Note: this is Spring's `@Validated`, not `jakarta.validation.Valid`. `@Validated` also carries the validation-**groups** value used to select groups.) 2. **`MethodValidationPostProcessor`** — a `BeanPostProcessor` that scans beans, and for any annotated with `@Validated` (by default) creates an **AOP proxy** whose advice is a `MethodValidationInterceptor`. That interceptor delegates to the Jakarta `ExecutableValidator` (`validator.forExecutables()`). **What gets validated:** Constraint annotations directly on parameters (`@NotNull String name`, `@Size(max=10) String code`), and `@Valid` on a parameter to **cascade** into the object graph. Constraints on the **return value** (annotation on the method itself, e.g. `@NotNull public Order find(...)`) validate the result after the body runs. Cross-parameter constraints are also supported. **Failure behavior:** A violation throws **`jakarta.validation.ConstraintViolationException`** (note: NOT Spring's `MethodArgumentNotValidException`, which is the MVC binding exception). The exception's `getConstraintViolations()` returns the set. Parameter validation happens **before** the method body executes; return-value validation **after**. **Spring Boot:** `ValidationAutoConfiguration` registers a `MethodValidationPostProcessor` automatically when a provider is on the classpath, so usually you only add `@Validated` to your class. As of Spring Framework 6.1 / Boot 3.2 there's also built-in method validation on controllers without a post-processor, but the classic mechanism here is the post-processor + `@Validated`. **AOP gotchas (critical):** - **Proxy-based**, so only calls that go **through the proxy** are intercepted. **Self-invocation** (`this.method(...)` from within the same bean) bypasses validation. - The class must be a **Spring-managed bean**; `new`-ing it yourself gives no validation. - With JDK dynamic proxies the bean needs an interface, or set `proxyTargetClass`; Boot defaults to CGLIB class proxies so this is usually fine, but `final` classes/methods can't be proxied. - Putting `@Validated` on the **method** does nothing for triggering — it belongs on the **type** (its method-level use is only for selecting groups on MVC `@Valid` binding, a different feature). **@Validated vs @Valid:** `@Validated` triggers/configures method validation and selects groups; `@Valid` (Jakarta) marks a parameter/field for **cascade** and is the standard MVC trigger. In method validation you often use both: `@Validated` on the class, `@Valid` on a nested parameter. **When to use:** enforcing preconditions on service/component method arguments centrally, guarding public APIs of a module, validating configuration objects. Prefer it over scattering manual `if (x == null) throw`.

  • Why doesn't validation fire when one method of the same bean calls another annotated method?
    It's proxy-based AOP. Self-invocation (this.other(...)) goes straight to the target instance, bypassing the proxy and its MethodValidationInterceptor. Only calls arriving through the injected proxy reference are intercepted.
  • What exception is thrown, and how does it differ from MVC body validation?
    Method validation throws jakarta.validation.ConstraintViolationException. MVC @Valid request-body binding throws MethodArgumentNotValidException (a BindException subtype). Different mechanisms, different exceptions, different exception handlers.

saying these in an interview costs you the question

  • Putting @Validated only on the method expecting it to trigger validation
  • Expecting self-invoked methods to be validated
  • Confusing ConstraintViolationException with MethodArgumentNotValidException
  • Thinking @Valid on a class enables method validation (it's @Validated on the type)

context