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.