I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we’re going to take a look at how we can create a custom validation annotation that uses a regular expression retrieved from a database to match against the field value.

We will use Hibernate Validator as a base implementation.

2. Maven Dependencies

For development, we will need the following dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
    <version>1.5.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>1.5.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.194</version>
</dependency>

The latest versions of spring-boot-starter-thymeleaf, spring-boot-starter-data-jpa, and h2 can be downloaded from Maven Central.

3. Custom Validation Annotation

For our example, we will create a custom annotation called @ContactInfo that will validate a value against a regular expression retrieved from a database. We will then apply this validation on the contactInfo field of a POJO class called Customer.

To retrieve regular expressions from a database, we will model these as a ContactInfoExpression entity class.

3.1. Data Models and Repository

Let’s create the Customer class with id and contactInfo fields:

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String contactInfo;

    // standard constructor, getters, setters
}

Next, let’s take a look at the ContactInfoExpression class – which will hold the regular expression values in a property called pattern:

@Entity
public class ContactInfoExpression {

    @Id
    @Column(name="expression_type")
    private String type;
 
    private String pattern;

    //standard constructor, getters, setters
}

Next, let’s add a repository interface based on Spring Data to manipulate the ContactInfoExpression entities:

public interface ContactInfoExpressionRepository 
  extends Repository<ContactInfoExpression, String> {
 
    Optional<ContactInfoExpression> findOne(String id);
}

3.2. Database Setup

For storing regular expressions, we will use an H2 in-memory database with the following persistence configuration:

@EnableJpaRepositories("com.baeldung.dynamicvalidation.dao")
@EntityScan("com.baeldung.dynamicvalidation.model")
@Configuration
public class PersistenceConfig {

    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        EmbeddedDatabase db = builder.setType(EmbeddedDatabaseType.H2)
          .addScript("schema-expressions.sql")
          .addScript("data-expressions.sql")
          .build();
        return db;
    }
}

The two scripts mentioned are used for creating the schema and inserting the data into the contact_info_expression table:

CREATE TABLE contact_info_expression(
  expression_type varchar(50) not null,
  pattern varchar(500) not null,
  PRIMARY KEY ( expression_type )
);

The data-expressions.sql script will add three records to represent the types email, phone, and website. These represent regular expressions for validating that value is a valid email address, a valid US phone number, or a valid URL:

insert into contact_info_expression values ('email',
  '[a-z0-9!#$%&*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?')
insert into contact_info_expression values ('phone',
  '^([0-9]( |-)?)?(\(?[0-9]{3}\)?|[0-9]{3})( |-)?([0-9]{3}( |-)?[0-9]{4}|[a-zA-Z0-9]{7})$')
insert into contact_info_expression values ('website',
  '^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$')

3.3. Creating the Custom Validator

Let’s create the ContactInfoValidator class that contains the actual validation logic. Following Java Validation specification guidelines, the class implements the ConstraintValidator interface and overrides the isValid() method.

This class will obtain the value of the currently used type of contact info — email, phone, or website — which is set in a property called contactInfoType, then use it to retrieve the regular expression’s value from the database:

public class ContactInfoValidator implements ConstraintValidator<ContactInfo, String> {
    
    private static final Logger LOG = Logger.getLogger(ContactInfoValidator.class);

    @Value("${contactInfoType}")
    private String expressionType;

    private String pattern;
 
    @Autowired
    private ContactInfoExpressionRepository expressionRepository;

    @Override
    public void initialize(ContactInfo contactInfo) {
        if (StringUtils.isEmptyOrWhitespace(expressionType)) {
            LOG.error("Contact info type missing!");
        } else {
            pattern = expressionRepository.findOne(expressionType)
              .map(ContactInfoExpression::getPattern).get();
        }
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (!StringUtils.isEmptyOrWhitespace(pattern)) {
            return Pattern.matches(pattern, value);
        }
        LOG.error("Contact info pattern missing!");
        return false;
    }
}

The contactInfoType property can be set in the application.properties file to one of the values email, phone or website:

contactInfoType=email

3.4. Creating the Custom Constraint Annotation

And now, let’s create the annotation interface for our custom constraint:

@Constraint(validatedBy = { ContactInfoValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface ContactInfo {
    String message() default "Invalid value";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

3.5. Applying the Custom Constraint

Finally, let’s add validation annotations to the contactInfo field of our Customer class:

public class Customer {
    
    // ...
    @ContactInfo
    @NotNull
    private String contactInfo;
    
    // ...
}

4. Spring Controller and HTML Form

To test our validation annotation, we will create a Spring MVC request mapping that uses the @Valid annotation to trigger the validation of a Customer object:

@PostMapping("/customer")
public String validateCustomer(@Valid Customer customer, BindingResult result, Model model) {
    if (result.hasErrors()) {
        model.addAttribute("message", "The information is invalid!");
    } else {
        model.addAttribute("message", "The information is valid!");
    }
    return "customer";
}

The Customer object is sent to the controller from an HTML form:

<form action="customer" method="POST">
Contact Info: <input type="text" name="contactInfo" /> <br />
<input type="submit" value="Submit" />
</form>
<span th:text="${message}"></span>

To wrap it all up, we can run our application as a Spring Boot application:

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

5. Conclusion

In this example, we have shown how we can create a custom validation annotation that retrieves a regular expression dynamically from a database and uses it to validate the annotated field.

The full source code of the example can be found over on GitHub.

I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE LESSONS

Sort by:   newest | oldest | most voted
Slava Semushin
Guest
Thanks for the write-up! I’d like to share some thoughts that came to my mind when I was reading it: – PersistenceConfig is defining bean with JdbcTemplate but I don’t see a reason to do so. First, Spring Boot has auto-configuration class for instantiating JdbcTemplate and, second, we don’t use JdbcTemplate in example, so most likely you can safely remove this bean. – ContactInfoValidator will always return false when expressionType is empty. That’s could be confusing because it marks even valid data as invalid and doesn’t provide any details neither to user nor to the log files. Because it’s a… Read more »
Loredana Crusoveanu
Guest

Hey Slava, thanks for the comments! I will respond to each in order:
– good point, we will update the article shortly
– the initialize() method is not run during application startup so throwing an exception there would not cause the startup to fail. If the property is missing, then the @Value injection will fail. We could add a log entry if the property is present but empty.
– sure, we could add that optimization
– it will return false for null values

Slava Semushin
Guest

> – the initialize() method is not run during application startup

Thank you for the information, I didn’t know that and expected a different behavior.

> – it will return false for null values

“Note that the Bean Validation specification recommends to consider null values as being valid. If null is not a valid value for an element, it should be annotated with @NotNull explicitly.” (c) https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-constraint-validator

Loredana Crusoveanu
Guest

Added @NotNull annotation 🙂