1. Hibernate Validator

Bean validation via Hibernate Validator.

1.1. Usage

1) Add the dependency:

Maven
Gradle
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-hibernate-validator</artifactId>
  <version>3.5.3</version>
</dependency>

2) Install

Java
Kotlin
import io.jooby.hibernate.validator.HibernateValidatorModule;

{
  install(new HibernateValidatorModule());
}

3) Usage in MVC routes

Java
Kotlin
import io.jooby.annotation.*;
import jakarta.validation.Valid;

@Path("/mvc")
public class Controller {

  @POST("/validate-body")
  public void validateBody(@Valid Bean bean) {                 (1)
    ...
  }

  @POST("/validate-query")
  public void validateQuery(@Valid @QueryParam Bean bean) {    (2)
    ...
  }

  @POST("/validate-list")
  public void validateList(@Valid List<Bean> beans) {          (3)
    ...
  }

  @POST("/validate-map")
  public void validateMap(@Valid Map<String, Bean> beans) {    (4)
    ...
  }
}
1 Validate a bean decoded from the request body
2 Validate a bean parsed from query parameters. This works the same for @FormParam or @BindParam
3 Validate a list of beans. This also applies to arrays @Valid Bean[] beans
4 Validate a map of beans

4) Usage in in script/lambda routes

Java
Kotlin
import io.jooby.validation.BeanValidator;

{
  use(BeanValidator.validate());
  post("/validate", ctx -> {
    Bean bean = ctx.body(Bean.class);
    ...
  });
}

BeanValidator.validate() behaves identically to validation in MVC routes. It also supports validating list, array, and map of beans

There is a handler version of it, so you can apply per route:

validate
import io.jooby.validation.BeanValidator.validate;

{
  post("/validate", validate(ctx -> {
    Bean bean = ctx.body(Bean.class);
    ...
  }));
}

1.2. Constraint Violations Rendering

HibernateValidatorModule provides default built-in error handler that catches ConstraintViolationException and transforms it into the following response:

JSON:
{
  "title": "Validation failed",
  "status": 422,
  "errors": [
    {
      "field": "firstName",
      "messages": [
        "must not be empty",
        "must not be null"
      ],
      "type": "FIELD"
    },
    {
      "field": null,
      "messages": [
        "passwords are not the same"
      ],
      "type": "GLOBAL"
    }
  ]
}

HibernateValidatorModule is compliant with ProblemDetails. Therefore, if you enable the Problem Details feature, the response above will be transformed into an RFC 7807 compliant format

It is possible to override the title and status code of the response above:

{
  install(new JacksonModule());
  install(new HibernateValidatorModule()
    .statusCode(StatusCode.BAD_REQUEST)
    .validationTitle("Incorrect input data")
  );
}

If the default error handler doesn’t fully meet your needs, you can always disable it and provide your own:

{
  install(new JacksonModule());
  install(new HibernateValidatorModule().disableViolationHandler());

  error(ConstraintViolationException.class, new MyConstraintViolationHandler());
}

1.3. Manual Validation

The module exposes Validator as a service, allowing you to run validation manually at any time.

1.3.1. Script/lambda:

import jakarta.validation.Validator;

{
  post("/validate", ctx -> {
    Validator validator = require(Validator.class);
    Set<ConstraintViolation<Bean>> violations = validator.validate(ctx.body(Bean.class));
    if (!violations.isEmpty()) {
      ...
    }
    ...
  });
}

1.3.2. MVC routes with dependency injection:

1) Install DI framework at first.

import io.jooby.hibernate.validator.HibernateValidatorModule;

{
  install(new GuiceModule());                 (1)
  install(new HibernateValidatorModule());
}
1 Guice is just an example, you can achieve the same with Avaje or Dagger

2) Inject Validator in controller, service etc.

import jakarta.validation.Validator;
import jakarta.inject.Inject;

@Path("/mvc")
public class Controller {

  private final Validator validator;

  @Inject
  public Controller(Validator validator) {
    this.validator = validator;
  }

  @POST("/validate")
  public void validate(Bean bean) {
    Set<ConstraintViolation<Bean>> violations = validator.validate(bean);
    ...
  }
}

1.4. Business rules validation

As you know, Hibernate Validator allows you to build fully custom ConstraintValidator. In some scenarios, you may need access not only to the bean but also to services, repositories, or other resources to perform more complex validations required by business rules.

In this case you need to implement a custom ConstraintValidatorFactory that will rely on your DI framework instantiating your custom ConstraintValidator

1) Implement custom ConstraintValidatorFactory:

public class MyConstraintValidatorFactory implements ConstraintValidatorFactory {

    private final Function<Class<?>, ?> require;
    private final ConstraintValidatorFactory defaultFactory;

    public MyConstraintValidatorFactory(Function<Class<?>, ?> require) {
        this.require = require;
        try (ValidatorFactory factory = Validation.byDefaultProvider()
                                          .configure().buildValidatorFactory()) {
            this.defaultFactory = factory.getConstraintValidatorFactory();
        }
    }

    @Override
    public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
        if (isBuiltIn(key)) {
            // use default factory for built-in constraint validators
            return defaultFactory.getInstance(key);
        } else {
            // use DI to instantiate custom constraint validator
            return (T) require.apply(key);
        }
    }

    @Override
    public void releaseInstance(ConstraintValidator<?, ?> instance) {
      if(isBuiltIn(instance.getClass())) {
        defaultFactory.releaseInstance(instance);
      } else {
        // No-op: lifecycle usually handled by DI framework
      }
    }

    private boolean isBuiltIn(Class<?> key) {
      return key.getName().startsWith("org.hibernate.validator");
    }
}

2) Register your custom ConstraintValidatorFactory:

{
  install(new HibernateValidatorModule().doWith(cfg -> {
    cfg.constraintValidatorFactory(new MyConstraintValidatorFactory(this::require));  (1)
  }));
}
1 This approach using require will work with Guice or Avaje. For Dagger, a bit more effort is required, but the concept is the same, and the same result can be achieved. Both Avaje and Dagger require additional configuration due to their build-time nature.

3) Implement your custom ConstraintValidator

public class MyCustomValidator implements ConstraintValidator<MyCustomAnnotation, Bean> {

  // This is the service you want to inject
  private final MyService myService;

  @Inject
  public MyCustomValidator(MyService myService) {
    this.myService = myService;
  }

  @Override
  public boolean isValid(Bean bean, ConstraintValidatorContext context) {
    // Use the injected service for validation logic
    return myService.isValid(bean);
  }
}

1.5. Configuration

Any property defined at hibernate.validator will be added automatically:

application.conf
hibernate.validator.fail_fast = true

Or programmatically:

import io.jooby.hibernate.validator.HibernateValidatorModule;

{
  install(new HibernateValidatorModule().doWith(cfg -> {
    cfg.failFast(true);
  }));
}