# 20 — Hibernate Validator CDI integration [← Previous: 19 — HikariCP connection pooling](19-hikaricp-connection-pooling.md) | [Back to README →](../README.md) Backs the rewrite of ankurm.com post 4887 (Hibernate Validator CDI integration). This is the odd chapter out in the batch: no Spring, no database, no Hibernate ORM at all. The question the original article raised is purely about Jakarta Bean Validation and CDI -- `@Inject` inside a `ConstraintValidator`, with and without a CDI container actually running -- so this chapter measures that in isolation, the same way chapter 17 isolated raw JPA bootstrap from Spring. ## The claim, and the two things it depends on A `ConstraintValidator` that needs a collaborator -- a policy object, a lookup service, anything that isn't a static constant -- naturally reaches for `@Inject`. Whether that actually works depends entirely on how the `ValidatorFactory` was built, not on the annotation itself: ```java public class PositiveInventoryValidator implements ConstraintValidator { @Inject private InventoryPolicy policy; // no null-guard, on purpose @Override public boolean isValid(Integer quantity, ConstraintValidatorContext context) { return quantity == null || quantity >= policy.minimumThreshold(); } } ``` [`PositiveInventoryValidator.java`](../src/main/java/com/ankurm/hibernatedemo/validation/PositiveInventoryValidator.java) `InventoryPolicy` is a plain `@ApplicationScoped` CDI bean with one method, `minimumThreshold()`, returning `5` -- deliberately not a hardcoded constant in the validator itself, so injection either genuinely happens or the validator has nothing to call. [`InventoryPolicy.java`](../src/main/java/com/ankurm/hibernatedemo/validation/InventoryPolicy.java) ## Without CDI: `@Inject` is not processed at all, and the failure is a plain NPE ```java ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); validator.validate(new StockLevel(3)); // throws ``` [`PlainValidationNoCdiTest.java`](../src/test/java/com/ankurm/hibernatedemo/validation/PlainValidationNoCdiTest.java) ``` RESULT[cdi-plain-validation-no-injection]: validating StockLevel(3) with Validation.buildDefaultValidatorFactory() (no CDI container running) throws jakarta.validation.ValidationException -> caused by java.lang.NullPointerException ``` [(full transcript)](output/20-plain-validation-no-cdi.txt) `Validation.buildDefaultValidatorFactory()`'s default `ConstraintValidatorFactory` builds a validator with plain reflection -- roughly `Class.getDeclaredConstructor().newInstance()`. It has no idea what CDI or `@Inject` even are; the annotation is simply never looked at, so `policy` stays `null`. The `NullPointerException` from calling `policy.minimumThreshold()` is not a validation failure -- it's a validator bug, and Hibernate Validator itself is honest about that: it wraps the unexpected exception in a `jakarta.validation.ValidationException` rather than letting the raw NPE escape unannounced, which is exactly what the transcript shows.
Trap: this failure only shows up when the constraint actually runs against a non-null value. A validator that only reaches the injected field on certain code paths can pass every test that happens not to exercise those paths, then NPE the first time production data takes the other branch.
## With CDI: the same code, actually injected ```java WeldContainer container = new Weld().initialize(); Validator validator = container.select(Validator.class).get(); validator.validate(new StockLevel(3)); // 1 violation validator.validate(new StockLevel(5)); // 0 violations validator.validate(new StockLevel(10)); // 0 violations ``` [`CdiValidationTest.java`](../src/test/java/com/ankurm/hibernatedemo/validation/CdiValidationTest.java) ``` RESULT[cdi-validation-injection-works]: validator obtained from a running Weld SE container | StockLevel(3) violations=1 | StockLevel(5) violations=0 | StockLevel(10) violations=0 -- InventoryPolicy.minimumThreshold()=5 was actually injected and actually used, no NullPointerException anywhere. ``` [(full transcript)](output/20-cdi-validation-injection.txt) The mechanism, not just the result: `hibernate-validator-cdi-9.1.3.Final.jar` registers `org.hibernate.validator.cdi.ValidationExtension` as a `jakarta.enterprise.inject.spi.Extension` (confirmed by its own `META-INF/services/jakarta.enterprise.inject.spi.Extension` file). Once Weld SE discovers that extension on the classpath, it contributes CDI beans for `Validator` and `ValidatorFactory` whose `ConstraintValidatorFactory` is `org.hibernate.validator.cdi.spi.InjectingConstraintValidatorFactory` -- a factory that builds each `ConstraintValidator` instance through the CDI `BeanManager` instead of plain reflection, resolving `@Inject` fields the same way any other managed bean's are resolved. Get the `Validator` from `Validation.buildDefaultValidatorFactory()` even while a CDI container happens to be running elsewhere in the same JVM, and you're back to the first, broken case -- what matters is *which* `ValidatorFactory` built the validator, not merely whether a container exists somewhere.
Trap: a project can have Weld or another CDI implementation on its classpath and still get the plain, non-injecting behavior everywhere it calls Validation.buildDefaultValidatorFactory() directly instead of obtaining the Validator/ValidatorFactory as a CDI-managed bean.
- Going deeper: this repo pins `hibernate-validator-cdi`, `weld-se-core`, and `org.glassfish.expressly` to specific, mutually-verified versions in [`pom.xml`](../pom.xml) -- `hibernate-validator-cdi:9.1.3.Final` requires `jakarta.enterprise.cdi-api:4.1.0` (checked against its own `pom.xml`), and `weld-se-core:6.0.4.Final` is the version that actually resolves that exact 4.1.0, confirmed with a throwaway `mvn dependency:tree` before writing any of this chapter's code. - Going deeper: `hibernate-validator-cdi` also ships a method-validation interceptor (`ValidationInterceptor`, visible in the jar's contents) for validating `@Valid` parameters on CDI-managed bean methods -- out of scope for this chapter, which is about constructor/field injection into the validator itself, not method interception. ## Production checklist - If a `ConstraintValidator` needs a collaborator, know which `ValidatorFactory` will actually build it in production. In a full Jakarta EE server or a Spring Boot app with `spring-boot-starter-validation` (which wires its own Spring-aware `ConstraintValidatorFactory`, a separate mechanism from the CDI one measured here), injection works by a different, already container-managed path -- this chapter's contrast is specifically about the CDI portable extension versus the plain, no-container default. - Never let a validator dereference an `@Inject`ed field without at least considering what happens if it's ever built by a plain `ConstraintValidatorFactory` -- a defensive null-check turns an opaque `ValidationException` into a clear "this validator requires CDI" message. - `hibernate-validator-cdi` is a thin CDI portable extension over `hibernate-validator` itself, not a different validation engine -- adding it changes *how* validators are instantiated, not what Bean Validation itself does. [← Previous: 19 — HikariCP connection pooling](19-hikaricp-connection-pooling.md) | [Back to README →](../README.md)