Skip to content

avaje/avaje-validator

Repository files navigation

Discord Build native image build Maven Central javadoc License

Reflection-free pojo validation via apt source code generation. A light (~120kb + generated code) source code generation style alternative to Hibernate Validation. (code generation vs reflection)

  • Annotate java classes with @Valid (or use @ImportValidPojo for types we "don't own" such as external dependencies)
  • avaje-validator-generator annotation processor generates Java source code to write validation classes
  • Supports Avaje/Jakarta/Javax Constraint Annotations
  • Groups Support
  • Class level Constraints
  • Composable Contraint Annotations
  • Inheritable Constraints
  • loading and interpolating error messages (with multiple Locales) through ResourceBundles
  • Getter Validation
  • Method parameter validation (requires a DI container to retrieve the generated MethodAdapter classes)

Quick Start

Step 1 - Add dependencies

<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-validator</artifactId>
  <version>${avaje.validator.version}</version>
</dependency>
<!-- Alternatively can use Jakarta/Javax Constraints-->
<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-validator-constraints</artifactId>
  <version>${avaje.validator.version}</version>
</dependency>

And add avaje-validator-generator as an annotation processor.

<!-- Annotation processors -->
<dependency>
  <groupId>io.avaje</groupId>
  <artifactId>avaje-validator-generator</artifactId>
  <version>${avaje.validator.version}</version>
  <optional>true</optional>
  <scope>provided</scope>
</dependency>

Step 2 - Add (Avaje/Jakarta/Javax) @Valid

Add @Valid to the types we want to add validation.

The avaje-validator-generator annotation processor will generate validation adapter classes as Java source code for each type annotated with @Valid. These will be automatically registered with Validator when it is started using a service loader mechanism.

@Valid
public class Address {
  @NotBlank
  private String street;

  @NotEmpty(message="must not be empty")
  private List<@NotBlank(message="{message.bundle.key}") String> suburb;

  @Valid
  @NotNull(groups=SomeGroup.class)
  private OtherClass otherclass;

  //add getters/setters
}

It also works with records:

@Valid
public record Address(
      @NotBlank String street,
      @NotEmpty(message="must not be empty") String suburb,
      @NotNull(groups=SomeGroup.class) String city
      ) {}

For types we cannot annotate with @Valid we can place @ImportValidPojo on any class/package-info to generate the adapters.

Step 3 - Use

// build using defaults
Validator validator = Validator.builder().build();

Customer customer = ...;

// will throw a `ConstraintViolationException` containing all the failed constraint violations
validator.validate(customer);

// validate with explicit locale
validator.validate(customer, Locale.ENGLISH);

// validate with groups
validator.validate(customer, Locale.ENGLISH, Group1.class);

Generated Code

Given the class:

@Valid
public record Address(
      @NotBlank String street,
      @NotEmpty(message="must not be empty") String suburb,
      @NotNull(groups=SomeGroup.class) String city
      ) {}

The following code will be generated and used for validation.

@Generated
public final class AddressValidationAdapter implements ValidationAdapter<Address> {

  private final ValidationAdapter<String> streetValidationAdapter;
  private final ValidationAdapter<String> suburbValidationAdapter;
  private final ValidationAdapter<String> cityValidationAdapter;

  public AddressValidationAdapter(ValidationContext ctx) {
    this.streetValidationAdapter =
        ctx.<String>adapter(NotBlank.class, Map.of("message","{avaje.NotBlank.message}"));
    this.suburbValidationAdapter =
        ctx.<String>adapter(NotEmpty.class, Map.of("message","must not be empty"));
    this.cityValidationAdapter =
        ctx.<String>adapter(NotNull.class, Map.of("message","{avaje.NotNull.message}", "groups",Set.of(example.avaje.typeuse.SomeGroup.class)));
  }

  @Override
  public boolean validate(Address value, ValidationRequest request, String propertyName) {
    if (propertyName != null) {
      request.pushPath(propertyName);
    }
    var _$street = value.street();
    streetValidationAdapter.validate(_$street, request, "street");

    var _$suburb = value.suburb();
    suburbValidationAdapter.validate(_$suburb, request, "suburb");

    var _$city = value.city();
    cityValidationAdapter.validate(_$city, request, "city");

    if (propertyName != null) {
      request.popPath();
    }
    return true;
  }
}