Mastering Hibernate Validator 7: Seamless CDI Bootstrapping for Enterprise Java

Are you tired of manually instantiating validators or dealing with NullPointerException when your custom constraint validators try to @Inject a service? In the world of modern Jakarta EE and MicroProfile applications, manual plumbing is a relic of the past. If you are moving to Hibernate Validator 7, understanding CDI bootstrapping is the key to building decoupled, testable, and robust validation layers.

The Problem: The Manual Validation Headache

Validation logic often needs access to external resources — database repositories, configuration services, or security contexts. When you use the standard Validation.buildDefaultValidatorFactory(), you are operating outside the CDI (Contexts and Dependency Injection) container. Your @Inject annotations within custom ConstraintValidator implementations simply won’t work. They return null, forcing you into anti-patterns like static lookups or manual dependency passing that make your code brittle and nearly impossible to unit test.

Imagine writing a @UniqueUsername constraint. You need your UserRepository to check the database. Without proper CDI bootstrapping, Hibernate Validator instantiates your validator class using simple reflection — bypassing the container’s dependency graph entirely. You end up with a validation layer that is “deaf” to your application’s ecosystem.

The Solution: Hibernate Validator 7 CDI Bootstrapping

Hibernate Validator 7 is the reference implementation of Jakarta Bean Validation 3.0. By bootstrapping the ValidatorFactory via CDI, the container becomes responsible for instantiating ConstraintValidator beans. This allows your validators to behave exactly like any other CDI-managed bean — with full @Inject support, interceptors, and decorators.

1. Prerequisites and Dependencies

Note that Hibernate Validator 7 moves to the jakarta.* namespace exclusively. If you need to stay on javax.*, you must remain on Hibernate Validator 6.x.

<dependencies>
    <!-- Hibernate Validator 7 (Reference Implementation) -->
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>8.0.1.Final</version>
    </dependency>

    <!-- CDI 4.0 API -->
    <dependency>
        <groupId>jakarta.enterprise</groupId>
        <artifactId>jakarta.enterprise.cdi-api</artifactId>
        <version>4.0.1</version>
        <scope>provided</scope>
    </dependency>

    <!-- Jakarta EL (required for dynamic message interpolation) -->
    <dependency>
        <groupId>org.glassfish</groupId>
        <artifactId>jakarta.el</artifactId>
        <version>4.0.2</version>
    </dependency>
</dependencies>

2. How CDI Bootstrapping Works Internally

The magic happens via the InjectingConstraintValidatorFactory. This implementation delegates validator creation to the CDI BeanManager. The process:

  1. The Validator identifies a constraint requiring a specific ConstraintValidator implementation.
  2. It calls the ConstraintValidatorFactory.
  3. The CDI-aware factory asks the BeanManager to find or create a bean of that type.
  4. CDI performs dependency injection, interceptors, and decorators on the validator.
  5. The fully managed bean is returned to Hibernate Validator.

3. Complete Example: Injecting a Service into a Validator

The Service (Business Logic):

package com.ankurm.service;

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class InventoryService {

    public boolean isSkuValid(String sku) {
        // In a real app, this queries a DB or external API
        return sku != null && sku.startsWith("PROD-") && sku.length() > 10;
    }
}

The Custom Constraint Annotation:

package com.ankurm.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SkuValidator.class)
@Documented
public @interface ValidSku {
    String message() default "Invalid SKU: Must start with PROD- and be at least 10 characters";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

The Validator (CDI-Managed):

package com.ankurm.validation;

import com.ankurm.service.InventoryService;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class SkuValidator implements ConstraintValidator<ValidSku, String> {

    @Inject
    private InventoryService inventoryService; // Works when bootstrapped via CDI!

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true; // Use @NotNull for null checks separately
        }
        return inventoryService.isSkuValid(value);
    }
}

// Usage on an entity field:
// @ValidSku
// private String sku;
//
// Validation output on "BAD-SKU":
// ConstraintViolation: "Invalid SKU: Must start with PROD- and be at least 10 characters"

4. Explicit Configuration: validation.xml

While CDI often handles things automatically in a Jakarta EE server, you can explicitly define your validator factory in META-INF/validation.xml to ensure consistency across different environments:

<?xml version="1.0" encoding="UTF-8"?>
<validation-config
    xmlns="https://jakarta.ee/xml/ns/validation/configuration"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/validation/configuration
                        https://jakarta.ee/xml/ns/validation/configuration/validation-configuration-3.0.xsd"
    version="3.0">

    <default-provider>org.hibernate.validator.HibernateValidator</default-provider>
    <executable-validation enabled="true"/>

</validation-config>

5. Bootstrapping in Java SE (Weld SE)

If you aren’t using a full application server, you can use Weld SE to get CDI benefits. Start the container manually to trigger the ValidationExtension:

import jakarta.enterprise.inject.se.SeContainer;
import jakarta.enterprise.inject.se.SeContainerInitializer;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import java.util.Set;

public class MainApp {
    public static void main(String[] args) {
        try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {

            Validator validator = container.select(Validator.class).get();

            // Product with a valid SKU
            Product validProduct = new Product("PROD-12345678");
            Set<ConstraintViolation<Product>> violations = validator.validate(validProduct);

            if (violations.isEmpty()) {
                System.out.println("Validation passed!");
                // Output: Validation passed!
            } else {
                violations.forEach(v -> System.out.println("Error: " + v.getMessage()));
            }

            // Product with an invalid SKU
            Product invalidProduct = new Product("BAD");
            violations = validator.validate(invalidProduct);
            violations.forEach(v -> System.out.println("Error: " + v.getMessage()));
            // Output: Error: Invalid SKU: Must start with PROD- and be at least 10 characters
        }
    }
}

Potential Pitfalls & Edge Cases

  • Missing beans.xml: In CDI 4.0, the discovery mode defaults have changed. To ensure your validators are picked up as CDI beans, include an empty beans.xml in META-INF or WEB-INF.
  • Scoped Validators: By default, ConstraintValidator beans are dependent-scoped. If you annotate them with @ApplicationScoped, ensure they are thread-safe, as Hibernate may reuse instances across concurrent validations.
  • Circular Dependencies: Be careful of circular injection loops (e.g., Service A injects Validator which injects Service A). CDI will throw a deployment error.
  • EL Implementation Conflicts: If you have multiple EL libraries on the classpath, Hibernate may fail to initialize. Use only one compliant Jakarta EL implementation.

Frequently Asked Questions

1. How do I enable Hibernate Validator in a Spring Boot 3 application?

In Spring Boot 3 (which uses Jakarta EE 9/10), include spring-boot-starter-validation. Spring Boot automatically provides a LocalValidatorFactoryBean configured to support Spring’s dependency injection within your ConstraintValidator classes, effectively mirroring CDI behavior.

2. Can I use @Inject in ConstraintValidator without a full application server?

Yes. Use Weld SE or OpenWebBeans SE. As long as the CDI container is initialized and hibernate-validator-cdi is on the classpath, @Inject will function as expected.

3. Why is my @Inject field null in a Custom Validator?

This happens when the Validator instance is created manually via Validation.buildDefaultValidatorFactory(). This method bypasses CDI. Fix it by injecting the Validator or ValidatorFactory using @Inject, or use CDI.current().select(Validator.class).get().

4. Does Hibernate Validator 7 support the old javax.validation namespace?

No. Hibernate Validator 7 exclusively uses the jakarta.validation namespace. If you need to remain on javax.*, stay on Hibernate Validator 6.x. Migration typically involves a bulk find-and-replace of import statements across your project — javax.validation → jakarta.validation — and updating annotation usages like @NotNull and @Valid which now come from the jakarta package.

Q5: How do I use CDI-bootstrapped validators in unit tests without a full container?

For unit tests, the cleanest approach is to use Weld SE as a lightweight CDI container. Initialise it in a @BeforeEach method, inject the Validator, run your assertions, and shut down the container in @AfterEach. Alternatively, if you only need to test the validator logic in isolation (without CDI), mock the injected service and call validator.isValid(value, context) directly after setting the mock via reflection or a constructor. For Spring Boot integration tests, annotate the test with @SpringBootTest — the application context provides a fully CDI-aware Validator automatically.

Conclusion

CDI bootstrapping transforms Hibernate Validator from a simple annotation processor into a fully integrated component of your application’s dependency graph. Once your ConstraintValidator beans are managed by CDI, they can inject services, repositories, security contexts, and any other application bean — turning complex cross-cutting validation rules into clean, testable, container-aware components. Remember: avoid Validation.buildDefaultValidatorFactory() in CDI environments, include an empty beans.xml if discovery is not automatic, and keep validators thread-safe when scoped beyond dependent scope. These patterns make your validation layer as robust and maintainable as any other part of your Jakarta EE application.

Further Reading & Cross-References

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.