Have you ever had the experience of binding properties with @ConfigurationProperties, only to crash at runtime because a value was empty or in an invalid format? You’d think that if it could be detected at startup, the failure could be prevented entirely.

In this article, we’ll walk through how to combine @ConfigurationProperties with Bean Validation to validate configuration values at application startup. We’ll also look at how to automate the validation logic with test code.

For the basics of using @ConfigurationProperties, see Spring Boot Properties Configuration Guide.

Why Validate at Startup

What makes configuration value issues tricky is that errors surface late. For example, if the database URL is an empty string, no error appears until you actually try to connect. It’s not uncommon to only notice the issue when a failure occurs right after a production release.

The Fail Fast philosophy says problems should be surfaced as early as possible. If you validate configuration values at startup, configuration mistakes are revealed the moment you deploy. If discovered in the development environment, the cost of fixing is minimal.

Add spring-boot-starter-validation

Since Spring Boot 2.3, spring-boot-starter-validation is no longer included in spring-boot-starter-web. You need to add the dependency explicitly.

<!-- Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'

This makes Hibernate Validator the implementation, enabling Bean Validation annotations.

Add @Validated to @ConfigurationProperties

The key point is to add @Validated to the configuration class in addition to @ConfigurationProperties. Without @Validated, validation won’t run even if you put constraint annotations on the fields.

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {

    @NotBlank
    private String name;

    @NotNull
    private Integer timeoutSeconds;

    // getter/setter
}

In Spring Boot 3.x, the package has changed from javax.validation to jakarta.validation. If you’re using the 2.x line, import from javax.validation.constraints.*.

For class registration, you can either annotate with @Component or declare @EnableConfigurationProperties(AppProperties.class) in a configuration class. If distributing as a library, @EnableConfigurationProperties is recommended (since it won’t be picked up by component scanning).

Examples of Common Constraint Annotations

Let’s look at an example combining annotations that are frequently used in practice.

@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {

    @NotBlank
    private String name;

    // URL format check via regex
    @Pattern(regexp = "https?://.+", message = "Please specify a valid URL")
    private String endpointUrl;

    // Range check: 1 to 300 seconds
    @Min(1)
    @Max(300)
    private int timeoutSeconds;

    // Disallow zero or below
    @Positive
    private int maxConnections;

    // getter/setter
}

@NotNull only rejects null, but @NotBlank also rejects null as well as empty strings and whitespace-only strings. For String fields, using @NotBlank is generally safer.

Propagating Validation to Nested Objects

It’s common to use inner classes to group settings like database configuration. In this case, be careful — if you forget @Valid, constraints on the inner class will be ignored. For the basics of using @Valid, see How to Simply Implement Validation with the Spring Boot @Valid Annotation.

@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {

    // @NotNull does the null check; @Valid propagates nested validation — different roles
    @NotNull
    @Valid
    private Database database;

    public static class Database {

        @NotBlank
        private String url;

        @Min(1)
        @Max(100)
        private int poolSize;

        // getter/setter
    }

    // getter/setter
}

The corresponding application.yml looks like this.

app:
  database:
    url: jdbc:postgresql://localhost:5432/mydb
    pool-size: 10

If you omit app.database entirely or set pool-size to 0, you’ll get an error at startup.

How to Read Startup Error Messages

When validation fails, a ConfigurationPropertiesBindException is thrown and output to the startup log in the following format.

APPLICATION FAILED TO START

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException:
Failed to bind properties under 'app' to com.example.AppProperties failed:

    Property: app.timeoutSeconds
    Value:    "0"
    Origin:   "app.timeout-seconds" from property source "application.yml" - 5:20
    Reason:   must be greater than or equal to 1

Action:

Update your application's configuration

Property: shows which field has the problem, Value: shows the actual bound value, Origin: shows where in application.yml the value was defined, and Reason: shows which constraint was violated. If multiple fields fail, the same block is repeated for each. Property: is displayed as the field name (camelCase), but Origin: shows the actual key name in kebab-case, making it easy to correlate with application.yml.

Testing Configuration Validation with @SpringBootTest

Let’s lock in the validation behavior with test code.

For the happy path, use @SpringBootTest. It starts the entire application context and is well-suited for integrated verification that configuration values are properly bound.

@SpringBootTest
@TestPropertySource(properties = {
    "app.name=MyApp",
    "app.endpoint-url=https://api.example.com",
    "app.timeout-seconds=30",
    "app.max-connections=5"
})
class AppPropertiesValidTest {

    @Autowired
    private AppProperties props;

    @Test
    void contextStartsWithValidConfiguration() {
        assertThat(props.getName()).isEqualTo("MyApp");
        assertThat(props.getTimeoutSeconds()).isEqualTo(30);
    }
}

For the failure path, use ApplicationContextRunner. Since it doesn’t start the entire context, it’s fast and well-suited for validating a configuration properties class in isolation. A good rule of thumb: use @SpringBootTest for full-context integration checks, and ApplicationContextRunner for lightweight, fast unit-level validation.

import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

class AppPropertiesInvalidTest {

    private final ApplicationContextRunner runner = new ApplicationContextRunner()
        .withUserConfiguration(TestConfig.class);

    @EnableConfigurationProperties(AppProperties.class)
    static class TestConfig {}

    @Test
    void startupFailsWhenRequiredFieldsMissing() {
        runner.withPropertyValues(
                "app.name=",             // empty string is invalid
                "app.timeout-seconds=0"  // less than 1 is invalid
            )
            .run(context ->
                assertThat(context).hasFailed()
            );
    }
}

Notes on Spring Boot 2.x and 3.x

Checklist When Validation Doesn’t Work or Startup Doesn’t Fail

“I added constraint annotations but no error at startup” is a common complaint. The cause almost always falls into one of these.

  • Forgot to add @Validated to the class: Even with @NotBlank and the like on fields, Spring won’t trigger validation without @Validated on the class.
  • Forgot @Valid on the nested target: Validation of inner class fields won’t propagate without @Valid on the parent field. Note that @NotNull and @Valid have different roles.
  • Mixing javax.validation and jakarta.validation: Spring Boot 3.x only reacts to jakarta.validation.constraints.*. If javax sneaks in from a copy-paste of old code, the annotations are present but silently ignored.
  • Missing spring-boot-starter-validation dependency: It’s not included in spring-boot-starter-web since 2.3. Use mvn dependency:tree to confirm it’s actually pulled in.
  • @ConfigurationProperties class not registered as a Bean: Without @Component or @EnableConfigurationProperties, values won’t be bound in the first place, and no validation will run.

If none of these help, the fastest path is to grep the startup log for ConfigurationPropertiesBindException. If the exception doesn’t appear, that’s a sign validation itself isn’t running; if it does appear, follow the “How to Read Startup Error Messages” section above and parse the Property: and Reason: fields.

FAQ

Q. How should I choose between @NotNull and @NotBlank?

A. For String fields, use @NotBlank by default. @NotNull only rejects null, so empty or whitespace-only values slip through. For non-String required fields like Integer or Boolean, use @NotNull.

Q. Can I use it with record classes?

A. In Spring Boot 3.x, you can apply @ConfigurationProperties and @Validated to record classes as-is. Constructor binding kicks in automatically, so @ConstructorBinding is unnecessary.

Q. What if I want to return a custom exception on validation errors?

A. Startup-time validation is structured around wrapping ConfigurationPropertiesBindException, but for shaping validation errors during HTTP requests, see Production-Ready GlobalExceptionHandler Implementation and How to Create Custom Validation Annotations.

Q. Should I write application.yml in camelCase or kebab-case?

A. Either binds, but Spring Boot’s official relaxed binding convention recommends kebab-case (timeout-seconds). It also aligns with the Origin: display in error logs, making it faster to locate the offending line in the configuration file.

The constructor binding syntax differs between versions.

2.x style puts @ConstructorBinding on the class.

// Spring Boot 2.x
@ConfigurationProperties(prefix = "app")
@ConstructorBinding
public class AppProperties {
    private final String name;
    private final int timeoutSeconds;

    public AppProperties(String name, int timeoutSeconds) {
        this.name = name;
        this.timeoutSeconds = timeoutSeconds;
    }
}

In 3.x style, you can omit @ConstructorBinding if there’s only one constructor. If there are multiple constructors, put it directly on the one you want bound.

// Spring Boot 3.x (@ConstructorBinding not needed with a single constructor)
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private final String name;
    private final int timeoutSeconds;

    public AppProperties(String name, int timeoutSeconds) {
        this.name = name;
        this.timeoutSeconds = timeoutSeconds;
    }
}

Also, 3.x uses the jakarta.validation package. When migrating from 2.x, don’t forget to do a bulk replace of import statements.

Summary

Adding validation to @ConfigurationProperties is straightforward.

  1. Add spring-boot-starter-validation to your dependencies
  2. Add @Validated to the @ConfigurationProperties class
  3. Add constraint annotations like @NotBlank or @Pattern to each field
  4. Add @Valid to nested objects to propagate validation

That’s all it takes to detect configuration mistakes immediately at application startup. By writing tests with ApplicationContextRunner, you can continuously guarantee the quality of configuration validation in CI.

If you’re switching configuration values by profile, see Safely Switching Environment-Specific Settings with Spring Boot Profiles. If you’re interested in encrypting configuration values, check out Encrypting Configuration Values with Jasypt as well.