This article is part of the “Quarkus for Spring Developers” series.

Validating data is a task that occurs across all layers, from presentation to persistence. If you are still doing it with dozens of if (obj.getField() == null) blocks, your application suffers from a “validation coupling” that makes code hard to read and error-prone. In the modern Java ecosystem, we use Jakarta Bean Validation 3.1, with Hibernate Validator 9.1 as the reference implementation.

In this detailed guide, we will transform your way of validating data, ranging from basic annotations to method validation and advanced constraints.


1. System Evolution: DTOs and Recursive Validation

Following our Orders project started in previous articles, we now use DTOs to protect our entities. The @Valid annotation on lists is what allows “cascading” validation.

OrderItemDTO.java

public class OrderItemDTO {
    @NotBlank(message = "Product code is mandatory")
    public String productCode;

    @Positive(message = "Quantity must be greater than zero")
    public int quantity;
}

OrderDTO.java

public class OrderDTO {
    @NotBlank(message = "Customer name is mandatory")
    public String customerName;

    @NotEmpty(message = "Order must have at least one item")
    public List<@Valid OrderItemDTO> items; // @Valid enables validation on list objects

    @PositiveOrZero(message = "Total cannot be negative")
    public double totalAmount;
}

In your JAX-RS Resource (Quarkus REST):

@POST
public Response create(@NotNull @Valid OrderDTO order) {
    // If code reaches here, data is 100% validated!
    return Response.status(Response.Status.CREATED).entity(order).build();
}

2. The Heart of Validation: Maven and Quarkus CLI

Unlike Spring Boot, where validation often comes in generic starters, in Quarkus we are explicit to ensure that the final binary (especially in Native mode with GraalVM) is optimized.

In Quarkus (Optimized Extension)

To add support for Hibernate Validator:

quarkus ext add hibernate-validator

This adds the io.quarkus:quarkus-hibernate-validator dependency to your pom.xml, which already configures the EL engine and CDI integration automatically.

In Pure Java Projects (Standalone)

Bean Validation is framework-agnostic. To use it in a library or CLI without Quarkus/Spring, you need the implementation and a Jakarta Expression Language (EL) engine to process dynamic messages.

<dependencies>
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>9.1.0.Final</version>
    </dependency>
    <!-- Required for Pure Java to process variables like {min} in messages -->
    <dependency>
        <groupId>org.glassfish.expressly</groupId>
        <artifactId>expressly</artifactId>
        <version>6.0.0</version>
    </dependency>
</dependencies>

3. The Defense Arsenal: Essential Annotations Table

The annotations below are the core of the specification. They allow you to describe what should be validated directly in the model.

AnnotationDescriptionExample
@NotNullField cannot be null.@NotNull String id;
@NotEmptyNot null and size > 0 (Strings, Collections, Maps).@NotEmpty List<@Valid Item> items;
@NotBlankNot null and contains at least one real character.@NotBlank String name;
@SizeDefines size limits for Strings or collections.@Size(min = 3, max = 50)
@Min / @MaxInclusive numerical limits.@Min(18) int age;
@Positive / @PositiveOrZeroValue must be greater than 0 (or >= 0).@Positive double price;
@Negative / @NegativeOrZeroValue must be less than 0 (or <= 0).@Negative double debt;
@DecimalMin / @DecimalMaxDecimal limits in String format.@DecimalMin("0.01")
@EmailValidates email format (RFC compliant).@Email String email;
@Past / @PastOrPresentDates in the past (e.g., birth date).@Past LocalDate birthday;
@Future / @FutureOrPresentDates in the future (e.g., delivery).@Future LocalDateTime delivery;
@DigitsValidates number of integer and fractional digits.@Digits(integer=5, fraction=2)
@PatternValidates against a Regular Expression (Regex).@Pattern(regexp = "^[A-Z0-9]+$")
@AssertTrue / @AssertFalseValidates boolean state.@AssertTrue boolean accepted;

Message Interpolation

You can use variables from the annotation itself in the messages:

@Size(min = 2, max = 14, message = "The plate '${validatedValue}' must be between {min} and {max} characters")
private String licensePlate;

4. Method Validation: Pre and Post-conditions

Few developers know, but you can validate parameters and return values of any CDI method. This is excellent for enforcing business rules at the service layer.

@ApplicationScoped
public class OrderService {

    public void processOrder(
        @NotNull @Valid OrderDTO order, 
        @Positive int priority
    ) {
        // Logic...
    }

    @NotNull @Size(min = 1)
    public List<Order> listRecentOrders() {
        return repository.findAll();
    }
}

If a rule is violated, Quarkus will throw a ConstraintViolationException.


5. Creating Your Own Rule: Custom Validation

When standard annotations are not enough, we create our own. In Quarkus, validators are CDI beans, allowing you to inject repositories.

Step 1: The Annotation

@Target({ FIELD, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = CustomerExistsValidator.class)
public @interface CustomerExists {
    String message() default "Customer not found";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Step 2: The Validator

@ApplicationScoped
public class CustomerExistsValidator implements ConstraintValidator<CustomerExists, Long> {
    @Inject CustomerRepository repository;

    @Override
    public boolean isValid(Long id, ConstraintValidatorContext context) {
        if (id == null) return true; // Let @NotNull handle mandatoriness
        return repository.findById(id) != null;
    }
}

6. Advanced Attributes: Groups and Payload

  • Groups: Allows validating different parts of the object at different times. E.g.:
    public interface OnCreate {}
    public interface OnUpdate {}
    
    public class User {
        @Null(groups = OnCreate.class)
        @NotNull(groups = OnUpdate.class)
        public Long id;
    }
    
  • Payload: Used to carry metadata. Useful for defining severity:
    @NotNull(payload = Severity.Error.class)
    public String criticalField;
    

7. Using Bean Validation without a Framework (Pure Java)

You can use the validation engine manually, which is useful in unit tests or scripts:

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

Set<ConstraintViolation<OrderDTO>> violations = validator.validate(myOrder);
if (!violations.isEmpty()) {
    violations.forEach(v -> System.out.println(v.getMessage()));
}

Senior Tip: Annotation Processor

To avoid silly mistakes like putting @Past on an int variable, add the Hibernate Validator Annotation Processor to your project. It will turn these errors into compilation failures.

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator-annotation-processor</artifactId>
    <version>9.1.0.Final</version>
</dependency>

Conclusion

Bean Validation is the professional way to ensure the integrity of your data without polluting your code with if-else. In Quarkus, this tool gains extra performance thanks to build-time optimizations. Stop writing manual validations and let the Jakarta engine work for you!


Resources