Jakarta Validation Integration
The fun-jakarta-validation module bridges Jakarta Bean Validation
with dmx-fun’s functional types. Instead of throwing ConstraintViolationException,
validation results are captured as Validated<NonEmptyList<E>, A>, keeping the
functional pipeline intact and surfacing all constraint violations at once.
It is an optional dependency — the core fun library has no runtime dependency
on Jakarta Validation. fun-jakarta-validation declares jakarta.validation-api as
compileOnly, following the same peer-dependency pattern used by fun-spring and
fun-jackson.
Adding the dependency
Any compliant Jakarta Validation 3.x provider is supported. Hibernate Validator is the most common choice; see version compatibility below.
// Gradleimplementation("codes.domix:fun-jakarta-validation:LATEST_VERSION")// Jakarta Validation — bring your own version (tested: 3.0.x – 3.1.x)implementation("jakarta.validation:jakarta.validation-api:3.1.1")// A Jakarta Validation provider, e.g. Hibernate Validator:implementation("org.hibernate.validator:hibernate-validator:9.1.0.Final")runtimeOnly("org.glassfish.expressly:expressly:6.0.0")<!-- Maven --><dependency> <groupId>codes.domix</groupId> <artifactId>fun-jakarta-validation</artifactId> <version>LATEST_VERSION</version></dependency><!-- Jakarta Validation — bring your own version (tested: 3.0.x – 3.1.x) --><dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> <version>3.1.1</version></dependency><!-- A Jakarta Validation provider, e.g. Hibernate Validator: --><dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>9.1.0.Final</version></dependency><dependency> <groupId>org.glassfish.expressly</groupId> <artifactId>expressly</artifactId> <version>6.0.0</version> <scope>runtime</scope></dependency>
fun-jakarta-validationdeclaresjakarta.validation-apiascompileOnly. You bring your own Jakarta Validation provider — any compliant implementation works (Hibernate Validator, Apache BVal, etc.).
API overview
DmxValidator is a static utility class with two methods:
| Method | Error type | When to use |
|---|---|---|
validate(validator, object, groups...) | NonEmptyList<String> | Display errors to users; messages are ready to render |
validateRaw(validator, object, groups...) | NonEmptyList<ConstraintViolation<T>> | Programmatic inspection of constraint metadata |
Both methods return Validated.valid(object) when all constraints pass, and
Validated.invalid(NonEmptyList.of(...)) when at least one constraint is violated.
Because the return type is Validated (not Result), all violations accumulate —
there is no fail-fast short-circuit.
DmxValidator
import dmx.fun.NonEmptyList;import dmx.fun.Validated;import dmx.fun.jakarta.validation.DmxValidator;import jakarta.validation.ConstraintViolation;import jakarta.validation.Validation;import jakarta.validation.Validator;import jakarta.validation.constraints.Min;import jakarta.validation.constraints.NotBlank;
record CreateItemRequest(@NotBlank String name, @Min(1) int quantity) {}
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
// ── validate — messages only ──────────────────────────────────────────────────
Validated<NonEmptyList<String>, CreateItemRequest> result = DmxValidator.validate(validator, new CreateItemRequest("", 0));
result .peekError(errors -> errors.stream().forEach(System.out::println)) // "name: must not be blank" // "quantity: must be greater than or equal to 1" .peek(item -> processItem(item));
// ── validateRaw — full ConstraintViolation detail ─────────────────────────────
Validated<NonEmptyList<ConstraintViolation<CreateItemRequest>>, CreateItemRequest> raw = DmxValidator.validateRaw(validator, new CreateItemRequest("", 0));
raw .peekError(violations -> violations.stream() .forEach(v -> log.warn("{} {}", v.getPropertyPath(), v.getMessage()))) .peek(item -> processItem(item));
// ── valid input ───────────────────────────────────────────────────────────────
Validated<NonEmptyList<String>, CreateItemRequest> ok = DmxValidator.validate(validator, new CreateItemRequest("widget", 5));
// ok.isValid() == true// ok.get() == CreateItemRequest("widget", 5)
// ── validation groups ─────────────────────────────────────────────────────────
interface OnCreate {}
DmxValidator.validate(validator, request, OnCreate.class);| Method | Returns | Valid input | Invalid input |
|---|---|---|---|
validate(validator, object, groups...) | Validated<NonEmptyList<String>, T> | Valid(object) | Invalid(messages) |
validateRaw(validator, object, groups...) | Validated<NonEmptyList<ConstraintViolation<T>>, T> | Valid(object) | Invalid(violations) |
Messages from validate have the form "propertyPath: constraintMessage" and are sorted
alphabetically for deterministic ordering. Violations from validateRaw are sorted by
property path.
Composing with other dmx-fun types
Validated integrates with Result and Try through the standard conversion methods:
// Validated → Result (NonEmptyList<String> as error)Result<User, NonEmptyList<String>> result = DmxValidator.validate(validator, cmd) .toResult();
// Flatten into a single pipeline: validate then persistResult<User, NonEmptyList<String>> saved = DmxValidator.validate(validator, cmd) .toResult() .flatMap(validCmd -> repo.save(validCmd));
// Combine two independent validated values (applicative style)Validated<NonEmptyList<String>, Order> order = DmxValidator.validate(validator, header) .combine(DmxValidator.validate(validator, lines), NonEmptyList::concat, Order::new);Choosing validate vs validateRaw
Use validate when:
- Errors are displayed to end users (API responses, form feedback).
- You only need the human-readable message and property path.
- You want a
NonEmptyList<String>that can be serialised directly to JSON.
Use validateRaw when:
- You need the constraint annotation type (
v.getConstraintDescriptor().getAnnotation()). - You need the invalid value (
v.getInvalidValue()). - You need to branch on a specific constraint class in programmatic logic.
Real-world example
A user registration service that validates a command record and returns all errors at once — username blank, email malformed, and password too short are reported together rather than one at a time.
import dmx.fun.NonEmptyList;import dmx.fun.Result;import dmx.fun.Validated;import dmx.fun.jakarta.validation.DmxValidator;import jakarta.validation.Valid;import jakarta.validation.Validator;import jakarta.validation.constraints.Email;import jakarta.validation.constraints.NotBlank;import jakarta.validation.constraints.Size;
// ── Domain model ──────────────────────────────────────────────────────────────
record RegisterUserCommand( @NotBlank String username, @Email @NotBlank String email, @Size(min = 8, max = 72) String password) {}
// ── Service ───────────────────────────────────────────────────────────────────
class UserRegistrationService {
private final Validator validator; private final UserRepo repo;
UserRegistrationService(Validator validator, UserRepo repo) { this.validator = validator; this.repo = repo; }
/** * Validates the command and, if valid, persists the new user. * * Returns all validation errors at once — no ConstraintViolationException escapes. */ public Result<User, NonEmptyList<String>> register(RegisterUserCommand cmd) { return DmxValidator.validate(validator, cmd) .toResult() // Validated → Result, errors as NonEmptyList<String> .flatMap(validCmd -> repo.save(validCmd) // Result<User, NonEmptyList<String>> ); }}
// ── Calling the service ───────────────────────────────────────────────────────
var result = service.register(new RegisterUserCommand("", "not-an-email", "short"));
result.fold( errors -> { // All three validation errors returned at once: // "email: must be a well-formed email address" // "password: size must be between 8 and 72" // "username: must not be blank" return Response.badRequest(errors.stream().toList()); }, user -> Response.created(user.id()));Version compatibility
The module is tested in CI against the following combinations on every pull
request that touches the jakarta-validation/ module:
| Jakarta Validation | Hibernate Validator | Status |
|---|---|---|
| 3.1.1 | 9.1.0.Final | tested |
| 3.1.1 | 9.0.1.Final | tested |
| 3.0.2 | 9.1.0.Final | tested |
| 3.0.2 | 9.0.1.Final | tested |
To test locally against a specific combination:
./gradlew :jakarta-validation:test \ -PjakartaValidationVersion=3.0.2 \ -PhibernateValidatorVersion=9.0.1.Final