Validating Configuration
Use @Validated and Bean Validation constraints on properties classes so bad configuration stops the application at startup, not at the first request.
@Validated
@ConfigurationProperties("app.prefix")
record Props(
@NotBlank String apiKey,
@Min(1) @Max(100) int pageSize,
@Valid Nested nested) { }Configuration errors are cheapest when they are found during deployment. A missing API key, a zero timeout or a pool size of 5000 should stop the application before it takes traffic, with a message naming the key. Spring Boot does this when you add @Validated to a @ConfigurationProperties class and put Bean Validation constraints on its fields or record components.
@Validated
@ConfigurationProperties("app.search")
record SearchProperties(@NotNull URI endpoint, @Min(1) @Max(100) int pageSize) { }What you need
spring-boot-starter-validationon the classpath, which brings Hibernate Validator. If only thejakarta.validationAPI arrives through another library, a@Validatedproperties class makes startup fail because no validation provider can be found.@Validated(fromorg.springframework.validation.annotation) on the properties class.- The constraints themselves, from
jakarta.validation.constraints:@NotBlank,@NotNull,@Min,@Max,@Pattern,@Emailand the rest.
Nested objects
Boot validates a nested record or class when keys under it are bound, so app.search.retry.max-attempts=-4 is rejected by the @Min(0) on Retry. Still annotate the field that holds it with @Valid: that is what makes the nested constraints run even when none of its keys are set, which is exactly the case where a required nested value is missing.
What failure looks like
Startup stops with a BindValidationException and the failure analyser prints the prefix, each failing key, the value it had, and the reason. In Kubernetes the pod goes into a crash loop and the old version keeps serving, which is far better than a running instance that fails every request needing the setting.
Rules that span fields
Single-field constraints cover most needs. For a rule such as "the minimum must be below the maximum", add a boolean method whose name starts with is and annotate it with @AssertTrue. Bean Validation treats it as a property and reports it with your message. Throwing an exception from a record's compact constructor also stops startup, but the error is a binding failure rather than a clear constraint message.
Mistakes
- Using
@NotNullon a primitiveint. A primitive is never null; an unset key becomes 0. Use@Min(1), orIntegerplus@NotNullif 'unset' must be an error. - Validating secrets with
@NotBlankbut giving them a default inapplication.properties. The check then always passes and the real value is never required. - Validating far more than matters. Check what would break the service or cause damage: URLs, credentials, limits and timeouts.
Example
import java.net.URI;
import java.time.Duration;
import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.validation.annotation.Validated;
("app.search")
public record SearchProperties(
URI endpoint,
String apiKey,
("20") (1) (100) int defaultPageSize,
("100") (1) (500) int maxPageSize,
("2s") Duration timeout,
Retry retry) {
public record Retry(("2") (0) (10) int maxAttempts) {
}
// A rule across two fields: reported as the property "pageSizesConsistent".
(message = "default-page-size must not exceed max-page-size")
public boolean isPageSizesConsistent() {
return defaultPageSize <= maxPageSize;
}
}When to use it
- Refusing to start a search service when its API key or endpoint URL has not been provided in a new environment.
- Keeping an operator from setting a connection or page size of zero or several thousand through an environment variable typo.
- Checking that a configured minimum order value is below the maximum before the checkout service takes traffic.
More examples
What a failed startup prints
Every failing key is listed at once, with the value and, where known, the file and line it came from. Fix them all in one pass rather than redeploying per error. The exact wording of property names in this report can vary, so search for the field name if the key does not match your file letter for letter.
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target com.example.search.SearchProperties failed:
Property: app.search.apiKey
Value: "null"
Reason: must not be blank
Property: app.search.retry.maxAttempts
Value: "-1"
Origin: class path resource [application.properties] - 14:28
Reason: must be greater than or equal to 0
Action:
Update your application's configurationA test that bad configuration is rejected
The runner starts a context with just the properties class and Bean Validation, so you can assert both that good settings start and that bad ones fail. Testing the failure path matters: it proves `@Validated` is present and the validation starter is on the classpath, which is exactly what goes missing when someone refactors.
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
@Validated
@ConfigurationProperties("app.sms")
record SmsProperties(@NotBlank String senderId, @Min(1) int maxPerMinute) {
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SmsProperties.class)
class SmsConfig {
}
class SmsPropertiesValidationTest {
private final ApplicationContextRunner runner =
new ApplicationContextRunner().withUserConfiguration(SmsConfig.class);
@Test
void startsWithValidSettings() {
runner.withPropertyValues("app.sms.sender-id=SHOP", "app.sms.max-per-minute=60")
.run(context -> assertThat(context).hasNotFailed().hasSingleBean(SmsProperties.class));
}
@Test
void refusesToStartWithoutASenderId() {
runner.withPropertyValues("app.sms.max-per-minute=60")
.run(context -> assertThat(context).hasFailed()
.getFailure().hasStackTraceContaining("senderId"));
}
}The dependency that makes it work
The constraint annotations come from the `jakarta.validation` API, which can arrive on the classpath through other libraries. Without an implementation such as Hibernate Validator, a `@Validated` properties class stops startup with an error saying no Jakarta Validation provider could be found. If you validate request bodies you already have this starter.
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Discussion