How do you cascade validation to nested @ConfigurationProperties objects, and what are the common reasons validation silently does nothing?
answer
- @Validated = on-switch (top class)
- @Valid = cascade into nested
- nested class needs no @Validated
- @Valid on collection -> each element
- silent no-op: no provider / lazy bean
basics
~20 sPut @Valid on the nested field/property to cascade constraints into the child object. Validation silently does nothing when there is no validation provider on the classpath, when the class lacks Spring's @Validated, or when the nested object isn't marked @Valid.
solid answer
~50 sTop-level validation is switched on by Spring's @Validated on the @ConfigurationProperties class, but that does not automatically descend into nested objects. To validate a nested config object you annotate the holding field (or constructor parameter) with jakarta.validation.@Valid, which tells the validator to cascade into it; the nested class doesn't need its own @Validated. For collections/maps of nested objects, @Valid on the collection cascades to each element. The classic silent-no-op causes are: (1) no Bean Validation provider on the classpath (Hibernate Validator missing) so nothing runs; (2) using @Valid instead of @Validated on the top-level class, so validation never activates; (3) forgetting @Valid on the nested holder so child constraints are skipped; (4) a lazily-created properties bean whose binding/validation is deferred. Constructor binding works too, with constraints and @Valid on the constructor parameters or record components.
code
java · 33 linesimport jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@ConfigurationProperties(prefix = "app")
@Validated // on-switch for the top-level object
public class AppProperties {
@Valid // cascade into the nested Security object
private final Security security = new Security();
@Valid // cascade into every element of the list
private List<Endpoint> endpoints = List.of();
public Security getSecurity() { return security; }
public List<Endpoint> getEndpoints() { return endpoints; }
public void setEndpoints(List<Endpoint> e) { this.endpoints = e; }
public static class Security { // no @Validated needed here
@NotBlank private String apiKey; // checked because parent field is @Valid
public String getApiKey() { return apiKey; }
public void setApiKey(String k) { this.apiKey = k; }
}
public static class Endpoint {
@NotEmpty private String url;
public String getUrl() { return url; }
public void setUrl(String u) { this.url = u; }
}
}go deeper
Know @Valid is needed on nested objects to check their constraints.
Distinguish @Validated (on-switch) from @Valid (cascade) and list a couple of silent-no-op causes.
Cover collection cascade, constructor binding, the full silent-no-op checklist, and cross-field custom validators.
Design a layered, fail-fast config surface with cohesive sub-objects and cross-field invariants, and guard against lazy-bean and classpath pitfalls in CI.
## Two different annotations, two different jobs - **`@Validated`** (`org.springframework.validation.annotation.Validated`) on the **top-level** `@ConfigurationProperties` class is the **on-switch** — without it, JSR-380 validation of the bound object never runs. - **`@Valid`** (`jakarta.validation.Valid`) is the **cascade marker** — placed on a field/parameter that holds another object, it tells the validator to recurse into that object and evaluate *its* constraints. They are not interchangeable. `@Validated` on the class + `@Valid` on nested holders is the correct combination. ## Cascading into nested objects Given a top-level `AppProperties` (`@Validated`) with a nested `Security` object, you must annotate the `security` field with `@Valid`; otherwise `Security`'s `@NotBlank apiKey` is ignored. The nested `Security` class itself does **not** need `@Validated` — the cascade carries the validation context down. This applies recursively to any depth. ## Collections and maps `@Valid` on a `List<Endpoint>` or `Map<String, Endpoint>` field cascades to **each element/value**, so per-element constraints are checked. ## Constructor binding / records With immutable (constructor-bound) properties, put constraints and `@Valid` on the **constructor parameters** or **record components**: ```java record AppProperties(@NotBlank String name, @Valid Security security) {} ``` ## The silent-no-op checklist (the interview meat) Validation quietly doing nothing is the recurring pain point. Causes: 1. **No provider on the classpath.** `@Validated` + constraints but no Hibernate Validator ⇒ nothing executes, no warning. Fix: add `spring-boot-starter-validation`. 2. **`@Valid` on the class instead of `@Validated`.** The on-switch is missing, so the whole object is unvalidated. 3. **Missing `@Valid` on the nested holder.** Top-level constraints fire, but nested ones are skipped. 4. **Lazy bean.** If the `@ConfigurationProperties` bean is `@Lazy`/only created on first use, binding + validation is deferred past startup, defeating fail-fast. Keep such beans eager. 5. **Constraint on the wrong element** — e.g. `@NotNull` on a primitive `int` (never null) instead of a range check, or `@NotNull` where `@NotBlank` is needed for empty strings. 6. **Wrong `@NotNull` package** — a non-Bean-Validation `@NotNull` (e.g. from `org.jetbrains.annotations` or `javax` on a Jakarta baseline) won't be enforced by the validator. ## Custom cross-field validation For invariants spanning multiple properties (e.g. `min <= max`), you can define a **custom validator bean named `configurationPropertiesValidator`** (a Spring `Validator`) that Boot applies to config beans, or a class-level custom JSR-380 constraint. ## When to use Use nested `@Valid` to keep a large config surface organized into cohesive sub-objects while still failing fast on any invalid leaf — the structure mirrors your YAML tree and every level is checked at boot.
- Does a nested @ConfigurationProperties sub-object also need its own @Validated?No. The nested class needs no @Validated; @Valid on the holding field/parameter in the parent cascades the validation context into it.
- How would you enforce an invariant across two properties, like min <= max?Use a class-level custom JSR-380 constraint, or register a Spring Validator bean named configurationPropertiesValidator that Boot applies to the config bean during binding.
saying these in an interview costs you the question
- Expecting nested constraints to fire without @Valid on the holder
- Putting @Validated on every nested class thinking it's required
- Not realizing a missing provider or a lazy bean makes validation a silent no-op
- Using @NotNull from the wrong package (jetbrains/other) and expecting enforcement