Hibernate Validator CDI: Inject Validator with Weld SE

Hibernate Validator CDI lets the CDI container create the ValidatorFactory and Validator. Learn to inject them with Weld SE, use @Inject inside a ConstraintValidator, and get automatic method validation on CDI beans.

Java bean validation

Hibernate Validator CDI is the module that lets a CDI container create the ValidatorFactory and Validator for us, so we get them with @Inject instead of building them with Validation.buildDefaultValidatorFactory(). CDI (Contexts and Dependency Injection) is the Jakarta EE standard for dependency injection, and Weld is its reference implementation. With the hibernate-validator-cdi jar on the classpath, CDI also injects beans into our custom ConstraintValidator classes and validates the method parameters and return values of CDI beans on every call.

We use the module in Java SE apps that run Weld, or in a Servlet container such as Tomcat, when a custom constraint needs a service or a repository to decide whether a value is valid.

The following example injects both objects and validates a TaxFiling, first with the injected Validator and then through a validated service method.

@Inject
Validator validator;                          // @Default qualifier

@Inject
@HibernateValidator
ValidatorFactory hibernateValidatorFactory;   // Hibernate Validator specific
Set<ConstraintViolation<TaxFiling>> none = validator.validate(new TaxFiling("lokesh", 2025, "52000.00", "4000.00"));   // no violations

Set<ConstraintViolation<TaxFiling>> violations = validator.validate(new TaxFiling("robin", 2027, "-10.00", "4000.00"));   // 3 violations:
// income must be greater than or equal to 0
// taxpayerId is not a registered taxpayer        (custom validator with an injected bean)
// year must be a date in the past or in the present

String receipt = taxFilingService.submit(invalidFiling);
// ConstraintViolationException: 3 constraint violation(s) occurred during method validation.

Notice that the second filing breaks three constraints, and one of them comes from a custom validator that uses an injected bean.

Next, we compare the CDI bootstrap with the plain one, set up Weld SE and go through injection, custom validators, method validation, validation.xml and tests.

1. Why Bootstrap the ValidatorFactory With CDI?

Bean Validation (Jakarta Validation) gives us two objects. The ValidatorFactory is the expensive, thread-safe object that holds the configuration, and the Validator comes from the factory and checks the constraints on an object. Without a container, our code builds the factory itself.

try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
  Validator validator = factory.getValidator();
  Set<ConstraintViolation<TaxFiling>> violations = validator.validate(filing);
}

The plain bootstrap works for the built-in constraints such as @NotNull, but it fails when a custom ConstraintValidator needs another object. For example, a tax filing app must reject filings from unknown taxpayers, and only a registry service knows which taxpayers exist. Hibernate Validator creates the validator class with its constructor, so the @Inject field for the registry stays null.

jakarta.validation.ValidationException: HV000028: Unexpected exception during isValid call.
Caused by: java.lang.NullPointerException: Cannot invoke "com.howtodoinjava.hibernate.validatorcdi.TaxpayerRegistry.isRegistered(String)" because "this.registry" is null

With the CDI module, Weld starts the ValidationExtension (a CDI portable extension, which is a plugin that the container loads at startup). The extension registers the factory and the validator as beans and builds every ConstraintValidator through CDI.

Two flows side by side: without CDI, Validation.buildDefaultValidatorFactory() creates RegisteredTaxpayerValidator with its constructor, the registry field is null and validation fails with HV000028 caused by a NullPointerException; with hibernate-validator-cdi, Weld starts the ValidationExtension, registers ValidatorFactory and Validator beans, injects the TaxpayerRegistry into the validator and returns a normal violation
The same constraint fails without CDI and works with it, because only CDI fills the @Inject field of the validator.

The two approaches also differ in who closes the factory and in method validation.

FeatureValidation.buildDefaultValidatorFactory()hibernate-validator-cdi
Who creates and closes the factoryOur codeThe CDI container
@Inject in a ConstraintValidatorIgnored, field is nullWorks
Method parameter and return value checksOnly by calling ExecutableValidator ourselvesAutomatic on CDI beans
Classes listed in validation.xmlCreated with their constructorCreated as CDI beans
Typical usePlain Java, no containerWeld SE, or Weld in a Servlet container

On a Jakarta EE server such as WildFly or Open Liberty, the CDI integration of Bean Validation is built in, so we do not add the jar there.

2. Hibernate Validator CDI Example

Our example is a small tax filing app on Java 25 with Hibernate Validator 9.1.4, Weld SE 6.0.4 and JUnit 6. Weld SE is the Java SE flavor of Weld, which runs a CDI container inside a main() method or a test.

2.1. Dependencies and beans.xml

We need four artifacts. Weld 6.0.x implements Jakarta CDI 4.1, the CDI version that hibernate-validator-cdi 9.1.4 is built against; Weld 7 implements CDI 5.0.

<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>9.1.4.Final</version>
</dependency>
<dependency>
  <groupId>org.hibernate.validator</groupId>
  <artifactId>hibernate-validator-cdi</artifactId>
  <version>9.1.4.Final</version>
</dependency>
<dependency>
  <groupId>org.jboss.weld.se</groupId>
  <artifactId>weld-se-core</artifactId>
  <version>6.0.4.Final</version>
</dependency>
<dependency>
  <groupId>org.glassfish.expressly</groupId>
  <artifactId>expressly</artifactId>
  <version>6.0.0</version>
</dependency>

Each artifact has its own job in the setup.

  • The hibernate-validator artifact is the Jakarta Validation 3.1 implementation.
  • The hibernate-validator-cdi artifact contains the ValidationExtension and the method validation interceptor.
  • The weld-se-core artifact is the CDI container. It brings the CDI 4.1 and jakarta.inject APIs.
  • The expressly artifact is an Expression Language implementation, which Hibernate Validator uses to build messages such as “must be greater than or equal to 0”.

Weld SE scans only the jars and folders that contain a META-INF/beans.xml file. With annotated discovery, every class with a scope annotation such as @ApplicationScoped becomes a bean.

<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       version="4.1" bean-discovery-mode="annotated">
</beans>

Starting and stopping the container takes one line each.

try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
  FilingInspector inspector = container.select(FilingInspector.class).get();
  var messages = inspector.check(filing);
}                                      // close() stops the container and the ValidatorFactory

2.2. The Tax Filing Model

A TaxFiling has a taxpayer id, a year, an income and deductions. Besides the built-in constraints, @RegisteredTaxpayer is our own constraint that checks the id against a registry.

@NotBlank
@RegisteredTaxpayer
private String taxpayerId;

@NotNull
@PastOrPresent
private Year year;

@NotNull
@PositiveOrZero
private BigDecimal income;

@NotNull
@PositiveOrZero
private BigDecimal deductions;

The registry is a normal CDI bean. In a real app it would query a database, whereas in the example it holds three ids.

@ApplicationScoped
public class TaxpayerRegistry {

  private final Set<String> taxpayers = Set.of("lokesh", "alex", "priya");

  public boolean isRegistered(String taxpayerId) {
    return taxpayers.contains(taxpayerId);
  }
}

2.3. Injecting Validator and ValidatorFactory

Any CDI bean can inject the two objects. The extension makes each of them available with two qualifiers (an annotation that tells CDI which of several beans of the same type we want).

@Inject
Validator validator;

@Inject
ValidatorFactory validatorFactory;

@Inject
@HibernateValidator
Validator hibernateValidator;

@Inject
@HibernateValidator
ValidatorFactory hibernateValidatorFactory;

An injection point without a qualifier uses @Default. The two qualifiers select different beans only when more than one Bean Validation provider is on the classpath.

QualifierWhat we getWhen to use it
@Default (no annotation)The default provider, as chosen by validation.xml or the classpathPortable code; works with any provider
@HibernateValidatorAlways Hibernate ValidatorWe need Hibernate-specific API, for example unwrap(HibernateValidatorFactory.class)

With Hibernate Validator as the only provider, both qualifiers return the same factory. The test checks that validatorFactory.unwrap(HibernateValidatorFactory.class) and the @HibernateValidator factory are the same object.

Import the qualifier from org.hibernate.validator.cdi.HibernateValidator. The class org.hibernate.validator.HibernateValidator has the same simple name, but it is the provider class used with Validation.byProvider(), not a qualifier.

We can also look the validator up from the container, which is handy in a main() method.

Validator validator = container.select(Validator.class).get();
int count = validator.validate(new TaxFiling("robin", 2027, "-10.00", "4000.00")).size();   // 3

2.4. Injecting a Bean Into a ConstraintValidator

The constraint annotation points to its validator class with @Constraint(validatedBy = …). The validator has a plain @Inject field.

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = RegisteredTaxpayerValidator.class)
public @interface RegisteredTaxpayer {
  String message() default "is not a registered taxpayer";
  Class<?>[] groups() default {};
  Class<? extends Payload>[] payload() default {};
}
public class RegisteredTaxpayerValidator implements ConstraintValidator<RegisteredTaxpayer, String> {

  @Inject
  TaxpayerRegistry registry;

  @Override
  public boolean isValid(String taxpayerId, ConstraintValidatorContext context) {
    if (taxpayerId == null) {
      return true;             // @NotBlank reports null values
    }
    return registry.isRegistered(taxpayerId);
  }
}

The validator class needs no scope annotation. We never inject the validator class anywhere, because the extension creates it through CDI and fills its @Inject fields. The same filing that failed with the plain factory gives a normal violation with the CDI factory.

var messages = inspector.check(new TaxFiling("robin", 2025, "100.00", "0.00"));
// [taxpayerId is not a registered taxpayer]

The Jakarta Validation specification forbids using an EntityManager inside a ConstraintValidator. Hibernate ORM validates entities during flush, so a query in the validator can trigger another flush. So we inject a service that reads the data in its own way, as TaxpayerRegistry does.

2.5. Automatic Method Validation on CDI Beans

Method validation checks the arguments of a method before it runs and its return value after it returns. On CDI beans, we only put the constraints on the method.

@ApplicationScoped
public class TaxFilingService {

  private String lastReceipt;

  public String submit(@NotNull @Valid TaxFiling filing) {
    lastReceipt = "Filed " + filing.getYear() + " for " + filing.getTaxpayerId();
    return lastReceipt;
  }

  public @PositiveOrZero BigDecimal taxableIncome(TaxFiling filing) {
    return filing.getIncome().subtract(filing.getDeductions());
  }
}

@Valid on the parameter tells the validator to check the constraints inside the TaxFiling object too, not only @NotNull on the reference. An interceptor is code that CDI runs around a bean method, through a generated subclass of the bean. No interceptor binding is needed, because the extension adds its ValidationInterceptor to every bean method that has constraints. CDI runs it at the interceptor priority 4800 that the specification sets.

Flow of a call to a CDI bean: the caller calls service.submit(), the Weld proxy passes it to the ValidationInterceptor with priority 4800, which validates the parameters; on a violation it throws ConstraintViolationException and the method body never runs, otherwise the method body runs, then the return value constraints are checked and either a ConstraintViolationException is thrown or the value is returned
Parameters are checked before the method body and return values after it. A violation never reaches our code.

A valid filing passes through, whereas an invalid one stops at the interceptor.

String receipt = service.submit(new TaxFiling("lokesh", 2025, "52000.00", "4000.00"));   // "Filed 2025 for lokesh"
String rejected = service.submit(new TaxFiling("robin", 2027, "-10.00", "4000.00"));     // ConstraintViolationException
String noFiling = service.submit(null);                                                 // ConstraintViolationException: submit.filing must not be null

BigDecimal taxable = service.taxableIncome(new TaxFiling("lokesh", 2025, "52000.00", "4000.00"));   // 48000.00
BigDecimal negative = service.taxableIncome(new TaxFiling("alex", 2025, "3000.00", "4000.00"));     // ConstraintViolationException

The exception message lists every violation with its property path (submit.filing.income), so it is useful in logs as it is.

jakarta.validation.ConstraintViolationException: 3 constraint violation(s) occurred during method validation.
Constructor or Method: public java.lang.String com.howtodoinjava.hibernate.validatorcdi.TaxFilingService.submit(com.howtodoinjava.hibernate.validatorcdi.TaxFiling)
Argument values: [TaxFiling[robin, 2027, -10.00, 4000.00]]
Constraint violations: 
 (1) Kind: PROPERTY
 message: is not a registered taxpayer
 root bean: com.howtodoinjava.hibernate.validatorcdi.TaxFilingService$Proxy$_$$_WeldSubclass@3db3ab68
 property path: submit.filing.taxpayerId
 ...
 (2) Kind: PROPERTY
 message: must be greater than or equal to 0
 property path: submit.filing.income
 ...
 (3) Kind: PROPERTY
 message: must be a date in the past or in the present
 property path: submit.filing.year

For the return value, the kind is RETURN_VALUE and the path is taxableIncome.<return value>. Whether a call is validated depends on how we got the object and on the kind of method.

SituationValidated?
Bean from the container (@Inject, container.select())Yes
Object created with new TaxFilingService()No, submit(invalid) returns “Filed 2027 for robin”
Getter such as getLastReceipt() with @NotNull return valueNo, getters are skipped by default
Getter with @ValidateOnExecution(type = ExecutableType.GETTER_METHODS)Yes, getCheckedReceipt.<return value> must not be null

The parameter name in the path comes from the compiled class. Without the -parameters compiler flag (maven.compiler.parameters set to true in Maven), the same violation reads submit.arg0.income.

2.6. Configuring the Injected Factory With validation.xml

The injected factory has no builder that we call, so we configure it in META-INF/validation.xml, which the extension reads when the container starts. Here we pick the provider and register a ClockProvider, which tells @PastOrPresent and @Future what “now” is.

<validation-config xmlns="https://jakarta.ee/xml/ns/validation/configuration" version="3.1">
  <default-provider>org.hibernate.validator.HibernateValidator</default-provider>
  <clock-provider>com.howtodoinjava.hibernate.validatorcdi.FixedClockProvider</clock-provider>
</validation-config>
public class FixedClockProvider implements ClockProvider {

  @Override
  public Clock getClock() {
    return Clock.fixed(Instant.parse("2026-04-15T10:00:00Z"), ZoneOffset.UTC);
  }
}

With the fixed clock, 2026 is the present year and 2027 is in the future.

var present = inspector.check(new TaxFiling("alex", 2026, "1.00", "0.00"));   // []
var future = inspector.check(new TaxFiling("alex", 2027, "1.00", "0.00"));    // [year must be a date in the past or in the present]

The same file can name a MessageInterpolator, TraversableResolver, ParameterNameProvider or ConstraintValidatorFactory. Under CDI, the specification requires all of them to be created as CDI beans, so they can use @Inject as well.

2.7. Testing With Weld SE in JUnit

A test starts one Weld SE container for the class and looks up the beans from it. The container finds the beans through the same beans.xml.

static SeContainer container;

@BeforeAll
static void startContainer() {
  container = SeContainerInitializer.newInstance().initialize();
}

@AfterAll
static void stopContainer() {
  container.close();
}

@Test
void invalidArgumentThrowsConstraintViolationException() {
  TaxFilingService service = container.select(TaxFilingService.class).get();

  ConstraintViolationException e = assertThrows(ConstraintViolationException.class,
      () -> service.submit(invalid));
  assertTrue(e.getMessage().startsWith("3 constraint violation(s) occurred during method validation."));
}

Always get the bean from the container in a test. A TaxFilingService created with new has no interceptor, so a test with it passes even when the validation is broken. The project on GitHub runs every case from the example (mvn -q compile exec:java) and checks them with 16 JUnit tests (mvn test).

3. Hibernate Validator CDI FAQs

3.1. Why Is My Injected Validator null?

CDI injects only into instances that the container creates, and only into instance fields. Three mistakes give either a null field or a startup error.

MistakeResult
@Inject static Validator validator;No error, the field stays null
new TaxFilingService() instead of injecting itNo injection and no method validation
No META-INF/beans.xml with Weld SEIllegalStateException: WELD-ENV-000016: Missing beans.xml file in META-INF

The static field version was common in older examples, and it never worked, because CDI injects only instance fields. We use an instance field in a bean, or look the validator up with container.select(Validator.class).get().

3.2. How Do I Make Hibernate ORM Use the CDI ValidatorFactory?

Hibernate ORM validates an @Entity before insert and update, in the pre-persist and pre-update lifecycle events. In Java SE it builds its own factory, which cannot inject into our validator. We pass the CDI factory with the jakarta.persistence.validation.factory property when we build the EntityManagerFactory.

EntityManagerFactory emf = new HibernatePersistenceConfiguration("filings")
    .managedClass(TaxFiling.class)
    .property(ValidationSettings.JAKARTA_VALIDATION_FACTORY, validatorFactory)
    // ... JDBC settings
    .createEntityManagerFactory();

The example runs it against an in-memory H2 database. With the property set, em.persist() of an unknown taxpayer fails before the INSERT, and the transaction rolls back.

jakarta.persistence.RollbackException
Caused by: jakarta.validation.ConstraintViolationException
  taxpayerId is not a registered taxpayer

Without the property, the same persist() of a registered taxpayer fails with HV000028 and the NullPointerException from section 1.

3.3. Do I Need hibernate-validator-cdi With Spring Boot or Jakarta EE?

No. Both bring their own integration.

  • A Jakarta EE server, for example WildFly or Open Liberty, contains the Bean Validation and CDI integration, so @Inject Validator and method validation work without extra jars.
  • Spring does not use CDI. Its LocalValidatorFactoryBean uses a SpringConstraintValidatorFactory, so ConstraintValidator classes get Spring beans with @Autowired. Method validation needs @Validated on the class, and Spring Boot sets it up when spring-boot-starter-validation is present.

We need hibernate-validator-cdi only with Weld SE in plain Java, or with Weld in a Servlet container such as Tomcat or Jetty.

4. Conclusion

With hibernate-validator-cdi and Weld SE, the CDI container owns the ValidatorFactory. We inject Validator with @Default or @HibernateValidator, and custom validators get their dependencies through @Inject. The interceptor checks the constrained methods of CDI beans before and after each call, and validation.xml configures the injected factory. In tests, we start Weld SE once and always take the beans from the container.

5. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.