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.

// Gradle
implementation("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-validation declares jakarta.validation-api as compileOnly. 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:

MethodError typeWhen 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);
MethodReturnsValid inputInvalid 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 persist
Result<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 ValidationHibernate ValidatorStatus
3.1.19.1.0.Finaltested
3.1.19.0.1.Finaltested
3.0.29.1.0.Finaltested
3.0.29.0.1.Finaltested

To test locally against a specific combination:

Terminal window
./gradlew :jakarta-validation:test \
-PjakartaValidationVersion=3.0.2 \
-PhibernateValidatorVersion=9.0.1.Final