How to Customize the @ConfigurationProperties Validation in Spring

6 minute read

It’s pretty common to use @ConfigurationProperties classes in Spring since they make it very easy to map externalized properties to a Java class. Also, they allow us to validate these properties at startup so we can avoid errors at runtime due to a wrong property configuration. This validation can be easily done just by using the standard JSR-303 annotations. However, sometimes we need to customize the validation to fulfill our requirements.

In this tutorial, we’ll create a @ConfigurationProperties class validated with JSR-303 annotations and after that, we’ll show how to create custom validators for our class.

Setup

For the purpose of this tutorial we’ll create a simple Spring Boot application with the minimal dependencies needed:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
  <version>2.3.2.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
  <version>2.3.2.RELEASE</version>
</dependency>

We’re using the spring-boot-starter-validation that includes the hibernate-validator which is probably the most used JSR-303 implementation.

After that, let’s create a minimal Application class:

@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

This application will be used in the next sections to illustrate our examples.

Creating our @ConfigurationProperties Class

Let’s create the @ConfigurationProperties class that we’re going to validate! This class will simply store some properties to save reports to a file and optionally send them by email:

@Validated
@ConfigurationProperties(prefix = "report")
public class ReportConfig implements Validator {

  @NotBlank
  private String targetFile;
  private boolean sendByEmail;
  private String emailSubject;
  private String recipient;

  // ... constructors, getters and setters

}

We’ve added the @Validated annotation to validate the targetFile property with @NotBlank. Note that this is a JSR-303 annotation.

After that, we’ll enable our class in our Application by using the @EnableConfigurationProperties annotation:

@SpringBootApplication
@EnableConfigurationProperties(ReportConfig.class)
public class Application {
  // ...
}

At this point, if we try to start our application and we haven’t set the report.targetFile property we get an error:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'report' to com.grabanotherbyte.spring.config.ReportConfig failed:

    Property: report.targetFile
    Value: null
    Reason: must not be blank


Action:

Update your application's configuration

As see above, the application doesn’t start and it gives a quite explanatory message.

Creating a Custom @ConfigurationProperties Validator

Now that we have our @ConfigurationProperties class ready we’re going to start customizing the validation process. At this moment, we’re only validating the targetFile but we’d also like to validate the emailSubject and recipient properties. However, we want to validate them only if sendByEmail is set to true. To do this we need to create a custom Validator since we can’t do that with annotations.

Let’s implement our custom validator now! To do so we’ll create a ReportConfigValidator class that implements the Spring interface Validator:

public class ReportConfigValidator implements Validator {

  @Override
  public boolean supports(Class<?> clazz) {
    return ReportConfig.class.isAssignableFrom(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    ReportConfig config = (ReportConfig) target;

    if (config.isSendByEmail()) {
      ValidationUtils.rejectIfEmpty(
          errors, "emailSubject", "required-non-empty", "Email subject is required");
      ValidationUtils.rejectIfEmpty(
          errors, "recipient", "required-non-empty", "Recipient is required");
    }
  }

As shown, this interface has only 2 methods and they are quite easy to implement:

  • boolean supports(Class<?> clazz): it’s used by Spring to find the validators that can validate a specific class. In our case, our Validator only supports our ReportConfig class
  • void validate(Object target, Errors errors): it validates the target object. As we can see, we need to cast this object to our class but if the supports method is implemented correctly this cast should be safe. All the failed validations are stored in the errors object that is received as a parameter

Also notice that we’re using the ValidationUtils class that Spring provides to help us with some common validations.

After that, we need to tell Spring to use this class to validate our @ConfigurationProperties classes. Let’s add it to our Application:

@Bean
public static ReportConfigValidator configurationPropertiesValidator() {
  return new ReportConfigValidator();
}

Notice that this bean has to be named as configurationPropertiesValidator or Spring won’t find it. It also has to be static since Spring needs to load it very early at the startup of the application.

Finally let’s create our properties and set the sendByEmail property to true:

report:
  sendByEmail: true

And let’s try to start the application afterward:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'report' to com.grabanotherbyte.spring.config.ReportConfig failed:

    Property: report.targetFile
    Value: null
    Reason: must not be blank

    Property: report.emailSubject
    Value: null
    Reason: Email subject is required

    Property: report.recipient
    Value: null
    Reason: Recipient is required

As expected, we’re getting the validation errors that we configured in our custom Validator plus the one we were getting before from the JSR-303 annotations.

Multiple Validators

The example shown in the previous section fits very well the cases where we only have 1 custom validator. Unfortunately, if we need to have more validators we can’t create more than 1 ConfigurationPropertiesValidator bean. One solution could be to use the same validator for all our @ConfigurationProperties classes but it can be a bit tedious if we have many classes. A more convenient solution would be to make our @ConfigurationProperties classes implement the Validator interface themselves.

Let’s do that for our ReportConfig class:

@Validated
@ConfigurationProperties(prefix = "report")
public class ReportConfig implements Validator {

  @NotBlank private String targetFile;
  private boolean sendByEmail;
  private String emailSubject;
  private String recipient;

  // ... constructors, getters and setters

  @Override
  public boolean supports(Class<?> clazz) {
    return ReportConfig.class.isAssignableFrom(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    ReportConfig config = (ReportConfig) target;

    if (config.isSendByEmail()) {
      ValidationUtils.rejectIfEmpty(
          errors, "emailSubject", "required-non-empty", "Email subject is required");
      ValidationUtils.rejectIfEmpty(
          errors, "recipient", "required-non-empty", "Recipient is required");
    }
  }
}

As we can see, it’s the same as we did in our ReportConfigValidator but just inside the @ConfigurationProperties class. If we had more classes to validate we’d also make them implement the Validator interface.

Now we can remove the configurationPropertiesValidator bean from our Application and try to start it again:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'report' to com.grabanotherbyte.spring.config.ReportConfig failed:

    Property: report.targetFile
    Value: null
    Reason: must not be blank

    Property: report.emailSubject
    Value: null
    Reason: Email subject is required

    Property: report.recipient
    Value: null
    Reason: Recipient is required

As expected, we get the same validation errors.

Accumulating Validators

In our previous example, we had removed the configurationPropertiesValidator since we moved that validator to the @ConfigurationProperties class. But what would happen if we left it? Let’s add it again and run the application:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'report' to com.grabanotherbyte.spring.config.ReportConfig failed:

    Property: report.emailSubject
    Value: null
    Reason: Email subject is required

    Property: report.recipient
    Value: null
    Reason: Recipient is required

    Property: report.targetFile
    Value: null
    Reason: must not be blank

    Property: report.emailSubject
    Value: null
    Reason: Email subject is required

    Property: report.recipient
    Value: null
    Reason: Recipient is required

We can see that now all our validators have been applied. And since 2 of them do the same we got duplicated validation errors.

This happens because Spring looks for validators in 3 different places:

  • In the ConfigurationPropertiesValidator bean
  • In the classpath if there is a JSR-303 validator and the target class is annotated with @Validated
  • In the target class if it implements the Validator interface

All these validators are accumulated when they are present and they all will be applied when a supported target object needs validation. In our latest example, we were using these 3 types of validators. It’s important to keep this in mind when we design our validators not to duplicate or contradict validations.

Conclusion

We’ve just seen how the validation of @ConfigurationProperties classes can be customized in Spring. We’ve presented some scenarios where custom validators are needed and provided a solution for them. We’ve finally explained where Spring looks for validators and how it accumulates them.

The source code of the examples is available at Github.

Leave a comment