Validation with Hibernate Validator
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Bean Validation/Hibernate Validator fundamentals — the built-in constraint annotation catalog, @Valid
cascading, custom constraints, constraint groups, and how the validator engine actually works internally — are
covered on the Hibernate Validator page and are
not repeated here. This page covers only the Quarkus-specific integration surface: how quarkus-hibernate-validator
wires that same Jakarta Validation engine into Quarkus REST resource methods, plain CDI beans, error responses,
and native-image builds.
Extension setup and REST payload validation
A project that needs Bean Validation pulls in quarkus-hibernate-validator:
./mvnw quarkus:add-extension -Dextensions=hibernate-validator
which adds the following to pom.xml:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
Once this extension is present, constraint annotations from jakarta.validation.constraints. on a Quarkus REST
resource method’s parameters are validated automatically, as already shown on
the Quarkus REST page's Bean Validation section. @Valid also applies
to a method’s *return value, cascading into a nested object graph on the way out — useful when an internal
service call could hand back a partially-populated object and the response should never leave the resource
method in a state that violates its own contract:
package com.example.book;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@Path("/books")
public class BookDetailResource {
@GET
@Path("/{id}")
@Valid
public BookDetail get(@PathParam("id") Long id) {
return BookRepository.findDetailById(id);
}
}
package com.example.book;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
public class BookDetail {
@NotBlank
private String title;
@NotNull
@Valid
private PublisherDetail publisher; // cascades into PublisherDetail's own constraints
public BookDetail(String title, PublisherDetail publisher) {
this.title = title;
this.publisher = publisher;
}
public String getTitle() {
return title;
}
public PublisherDetail getPublisher() {
return publisher;
}
}
package com.example.book;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
public class PublisherDetail {
@NotBlank
private String name;
@Positive
private int foundedYear;
public PublisherDetail(String name, int foundedYear) {
this.name = name;
this.foundedYear = foundedYear;
}
public String getName() {
return name;
}
public int getFoundedYear() {
return foundedYear;
}
}
@Valid on the resource method itself (rather than on a parameter) is what triggers this return-value check;
without it, BookDetail and the nested PublisherDetail could both violate their own constraints and the
response would still be sent as-is. A violation on the way out is reported the same way as a violation on the
way in — see error payloads, and native image below for the response shape and
how to customize it.
Validating CDI beans, constraint groups, and custom constraints
Method-level validation on any CDI bean
Method-level validation is not limited to REST resource methods — once quarkus-hibernate-validator is on the
classpath, Quarkus registers a CDI interceptor that validates annotated parameters and return values on any
CDI bean’s methods, not only ones reachable from an HTTP request:
package com.example.book;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
@ApplicationScoped
public class BookPricingService {
public BigDecimalQuote quote(@NotNull @Valid BookRequest request) {
return computeQuote(request);
}
private BigDecimalQuote computeQuote(BookRequest request) {
// ...
return new BigDecimalQuote(request.getTitle());
}
}
package com.example.book;
import jakarta.validation.constraints.NotBlank;
public class BookRequest {
@NotBlank
private String title;
public BookRequest(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
package com.example.book;
import java.math.BigDecimal;
public class BigDecimalQuote {
private final BigDecimal amount;
public BigDecimalQuote(String title) {
this.amount = BigDecimal.TEN;
}
public BigDecimal getAmount() {
return amount;
}
}
Calling quote(null), or with a BookRequest whose title is blank, throws
jakarta.validation.ConstraintViolationException from the interceptor before computeQuote ever runs — no
@Validated class-level annotation is needed, unlike the Spring MVC equivalent described on the Hibernate
Validator page; the interceptor applies to every CDI bean automatically once the extension is present.
Constraint groups
A marker interface distinguishes which set of constraints applies for a given validation call:
package com.example.book;
public interface OnCreate {
}
package com.example.book;
import jakarta.validation.constraints.Null;
import jakarta.validation.constraints.NotNull;
public class BookGroupedRequest {
@Null(groups = OnCreate.class)
private Long id;
@NotNull
private String title;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
package com.example.book;
import java.util.Set;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
@ApplicationScoped
public class BookGroupValidationService {
@Inject
Validator validator;
public void validateForCreate(BookGroupedRequest request) {
Set<ConstraintViolation<BookGroupedRequest>> violations =
validator.validate(request, OnCreate.class);
if (!violations.isEmpty()) {
throw new jakarta.validation.ConstraintViolationException(violations);
}
}
}
The injected Validator bean (covered in full under
error payloads, and native image) works exactly as documented on the Hibernate
Validator page — Quarkus contributes nothing extra to group handling itself, only the CDI-injectable
Validator instance and the automatic method-level interceptor above.
Custom constraints
A custom annotation paired with its ConstraintValidator implementation works identically to plain Jakarta
Validation, and needs no Quarkus-specific registration beyond having quarkus-hibernate-validator on the
classpath (see Native-image considerations for why this matters for native builds specifically):
package com.example.book;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = StrongPasswordValidator.class)
public @interface StrongPassword {
String message() default "must be at least 12 characters and contain a digit";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
package com.example.book;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && value.length() >= 12 && value.chars().anyMatch(Character::isDigit);
}
}
package com.example.book;
import jakarta.validation.constraints.NotBlank;
public class AccountRequest {
@NotBlank
private String username;
@StrongPassword
private String password;
public AccountRequest(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
}
Programmatic validation, error payloads, and native image
Programmatic validation
The jakarta.validation.Validator bean is CDI-injectable directly, for validating an object outside any
automatic parameter/return-value interception — useful when a service must decide for itself what to do with
the violations rather than letting an exception propagate:
package com.example.book;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
@ApplicationScoped
public class BookImportService {
@Inject
Validator validator;
public void importBook(BookRequest request) {
Set<ConstraintViolation<BookRequest>> violations = validator.validate(request);
if (!violations.isEmpty()) {
String summary = violations.stream()
.map(v -> v.getPropertyPath() + " " + v.getMessage())
.collect(Collectors.joining("; "));
throw new IllegalArgumentException("Invalid book: " + summary);
}
// proceed with a validated request
}
}
Each ConstraintViolation<T> in the returned Set carries the failing getPropertyPath(), the interpolated
getMessage(), getInvalidValue(), and the root bean/leaf bean involved — enough to build either a log entry
or a structured error response without re-inspecting the original object.
Localized messages
A constraint’s message attribute supports {…} placeholders, interpolated against the constraint’s own
attributes and, for locale-specific text, a ValidationMessages.properties resource bundle on the classpath
(with ValidationMessages_fr.properties, ValidationMessages_de.properties, and so on, for other locales):
package com.example.book;
import jakarta.validation.constraints.Size;
public class BookTitle {
@Size(min = 1, max = 200, message = "{book.title.size}")
private String title;
}
# src/main/resources/ValidationMessages.properties
book.title.size=Title must be between {min} and {max} characters
# src/main/resources/ValidationMessages_fr.properties
book.title.size=Le titre doit comporter entre {min} et {max} caractères
Hibernate Validator resolves the message for the caller’s current Locale (from ConstraintValidatorContext,
or the default JVM locale for programmatic Validator.validate calls) without any Quarkus-specific
configuration beyond the resource bundle files being present on the classpath.
Default error payload and customizing it
A validation failure on a Quarkus REST resource method — an invalid parameter, or an invalid return value once
@Valid is present, as in Extension setup and REST payload validation — produces a 400 Bad Request
by default, with a JSON body listing each violation’s field and message. Reshaping that body into the
application’s own error format uses the same jakarta.ws.rs.ext.ExceptionMapper mechanism already covered on
the Quarkus REST page, mapping jakarta.validation.ConstraintViolationException
specifically:
package com.example.book;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.validation.ConstraintViolationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
@Provider
public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(ConstraintViolationException exception) {
List<FieldError> errors = exception.getConstraintViolations().stream()
.map(v -> new FieldError(v.getPropertyPath().toString(), v.getMessage()))
.collect(Collectors.toList());
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorPayload("VALIDATION_FAILED", errors))
.type(MediaType.APPLICATION_JSON)
.build();
}
public record FieldError(String field, String message) {
}
public record ErrorPayload(String code, List<FieldError> errors) {
}
}
Registering this mapper (@Provider is enough, exactly as for any other exception type) replaces Quarkus’s
built-in violation-report body with the application’s own shape for every endpoint, without touching the
resource methods themselves.
Native-image considerations
Constraint annotations and any custom ConstraintValidator implementation must be resolvable at build time for
a native executable, since GraalVM’s closed-world assumption means nothing can be discovered by arbitrary
reflection or classpath scanning at runtime that wasn’t already known when the executable was built.
quarkus-hibernate-validator handles the common cases automatically: build-time processing scans the
application’s own classes for constraint annotations and registers the corresponding ConstraintValidator
classes (including custom ones like StrongPasswordValidator above) for reflection ahead of time, as part of
Quarkus’s normal augmentation phase — no manual reflection-config.json entry is needed for a validator
defined and used the ordinary way.
What does not work in a native executable is a validator resolved through some mechanism outside this
build-time processing — for instance, a ConstraintValidatorFactory that instantiates validator classes by
scanning an arbitrary package at runtime, or a validator class loaded by name from a string computed only at
runtime (from configuration, say). Both rely on reflection paths GraalVM’s native-image analysis cannot see
ahead of time, and both fail at runtime in the native executable (typically with a
ClassNotFoundException/InstantiationException) even though they work under quarkus:dev on the JVM. Keeping
constraints and their validators as ordinary annotated classes — discovered the way every example on this page
already is — stays inside what Quarkus’s extension actually processes at build time.