This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/docs by this push:
new 777ebf9cc0 docs: Jakarta Bean Validation topic page + 9.5.0
release-notes entry (TODO-68)
777ebf9cc0 is described below
commit 777ebf9cc081b724e5dd30cdf708633d1e55ecfe
Author: James Bognar <[email protected]>
AuthorDate: Tue May 26 16:07:54 2026 -0400
docs: Jakarta Bean Validation topic page + 9.5.0 release-notes entry
(TODO-68)
Co-authored-by: Cursor <[email protected]>
---
pages/release-notes/9.5.0.md | 79 +++++++++
pages/topics/10.20f.RestServerValidation.md | 256 ++++++++++++++++++++++++++++
sidebars.ts | 5 +
3 files changed, 340 insertions(+)
diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 49bb196ae6..79354fe746 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -3053,6 +3053,85 @@ public class AccountResource {
See [REST Server — RFC 7807 Problem
Details](/docs/topics/RestServerProblemDetails) for the full topic, including
worked examples for the resource-level opt-in, the per-op fanout, the
`ProblemException` throw path, and `ProblemMapper` registration.
+#### Jakarta Bean Validation 3.x integration (TODO-68)
+
+**Validation is opt-in and disabled by default.** This is a non-negotiable
contract: a fresh `RestContext` built with no validation-related markers
anywhere on the resource never instantiates a `Validator`, never invokes a
constraint check, and never adds a per-request cost — even when
`jakarta.validation-api` and a concrete provider (e.g. Hibernate Validator) are
on the classpath. Spring devs migrating in should be aware that, unlike Spring
MVC where `@Valid` is automatic, Juneau requir [...]
+
+##### Opt-in marker
+
+- **`@jakarta.validation.Valid`** on a `@Content` / `@FormData` /
`@Request`-bound parameter — turns on validation for that one parameter on that
one handler. Per-parameter granularity: marking one parameter never opts in a
sibling parameter, an unrelated handler, or the resource as a whole. Spring's
`@org.springframework.validation.annotation.Validated` and the legacy
`@javax.validation.Valid` are also recognized via fully-qualified-name (FQN)
detection, so no compile-time dependency on [...]
+
+```java
+@RestPost("/orders")
+public Order create(@Content @Valid OrderRequest in) {
+ // If any of OrderRequest's @NotBlank / @Size / @Min / etc. constraints
fail,
+ // the handler is never called. Response is 400 with the violations.
+ return orderService.create(in);
+}
+```
+
+##### Failure response shape
+
+- **Without `@Rest(problemDetails="true")`** (or when problem-details is off
for the op): `400 Bad Request` with `application/json` body of the form `{
"status": 400, "errors": [ { "path": "...", "message": "...", "constraint":
"..." }, ... ] }`. The shape mirrors Spring's `MethodArgumentNotValidException`
payload at the top level for migration friendliness.
+- **With `@Rest(problemDetails="true")`** (the RFC 7807 / 9457 path documented
above): `400 application/problem+json` with the standard problem fields
(`status`, `title`, `detail`) plus an `errors[]` extension carrying the same
`ValidationViolation` list.
+
+The `invalidValue` field of each violation is **omitted by default** for
privacy reasons. Handlers that need to surface the offending value (e.g. an
internal debugging endpoint) can include it explicitly by constructing the
violation with the `invalidValue` setter; the renderer honors a non-`null`
value verbatim.
+
+##### Dependency stance — optional, `provided` scope
+
+`juneau-rest-server`'s `pom.xml` declares `jakarta.validation-api:3.0.2` in
`provided` scope. Consumers who never write `@Valid` pay no transitive cost —
no Jakarta Validation, no Hibernate Validator, no Jakarta EL pulled in
automatically. Consumers who do opt in supply the runtime provider on their own
classpath:
+
+```xml
+<!-- in the consumer's pom.xml -->
+<dependency>
+ <groupId>org.hibernate.validator</groupId>
+ <artifactId>hibernate-validator</artifactId>
+ <version>8.0.3.Final</version>
+</dependency>
+<dependency>
+ <groupId>org.glassfish.expressly</groupId>
+ <artifactId>expressly</artifactId>
+ <version>5.0.0</version>
+</dependency>
+```
+
+##### Graceful degradation
+
+If a parameter carries `@Valid` but no Jakarta Validation provider is
reachable at runtime (the API JAR is on the classpath but the engine isn't),
the integration silently skips validation and emits a one-shot `WARNING` log
per JVM:
+
+```text
+Jakarta Bean Validation was requested via @Valid on a REST argument, but no
+jakarta.validation.Validator bean is registered and no runtime provider
+(e.g. org.hibernate.validator:hibernate-validator + jakarta.el implementation)
+is on the classpath. Validation will be silently skipped for this and
+subsequent requests. Add a provider dependency or register a Validator bean
+to enable validation.
+```
+
+The request continues through the handler with the bean unmodified — the
off-by-default contract is preserved even in misconfigured deployments. This
matches the FINISHED-24 FQN-detection precedent: feature presence is signaled
by the consumer (here via `@Valid`) but the engine is a runtime resolution that
fails open, not a startup-time hard requirement.
+
+##### `Validator` resolution order
+
+The integration consults, in order:
+
+1. A user-supplied `jakarta.validation.Validator` bean visible from the
resource's `BeanStore` (typically contributed via `@Bean public Validator
validator() { ... }` on the resource). Use this when the deployment needs
custom message interpolators, group sequences, or a non-default
`ConstraintValidatorFactory`.
+2. A lazily-built JVM-wide default obtained from
`Validation.buildDefaultValidatorFactory()`. Cached after the first successful
build so the factory cost is paid at most once per JVM.
+3. `null` — the graceful-degradation path described above.
+
+##### New API surface
+
+- **`org.apache.juneau.rest.validation.ValidationException`** — `BadRequest`
(400) subclass carrying an immutable `List<ValidationViolation>`. Thrown from
`ContentArg` / `FormDataArg` / `RequestBeanArg` after the bean is bound but
before the handler is called.
+- **`org.apache.juneau.rest.validation.ValidationViolation`** — POJO with
`path`, `message`, `constraint`, and optional `invalidValue` fields.
Serializable to JSON via Juneau's default serializers.
+- **`org.apache.juneau.rest.validation.BeanValidator`** — static dispatcher
that performs FQN-based opt-in detection and runs the validator. Acts as the
seam between the arg resolvers and the underlying
`jakarta.validation.Validator`.
+
+##### What's out of scope (v1)
+
+- **Method-level validation** (`@Validated` on the resource class, constraints
on `@RestOp` return values). The integration validates parameter beans only.
+- **Schema-driven validation** (JSON-Schema constraints on the request body).
That lives in `JsonSchemaGenerator` territory.
+- **`Messages.properties` bridge** between Juneau's `Messages` and Jakarta's
`ValidationMessages.properties`. The two bundles don't compose automatically;
document and configure separately.
+
+See [REST Server — Jakarta Bean Validation](/docs/topics/RestServerValidation)
for the full topic, including worked examples for the opt-in marker, the
problem-details and non-problem-details response shapes, custom `Validator`
registration, custom `Constraint` annotations, and the graceful-degradation
contract.
+
#### Conditional-GET / ETag helpers
`juneau-rest-server` now ships an RFC 7232 conditional-request layer so
handlers no longer need to hand-roll `ETag` / `Last-Modified` / `If-None-Match`
/ `If-Match` plumbing. The wiring is purely additive — existing handlers
continue to work unchanged.
diff --git a/pages/topics/10.20f.RestServerValidation.md
b/pages/topics/10.20f.RestServerValidation.md
new file mode 100644
index 0000000000..35e5eb995b
--- /dev/null
+++ b/pages/topics/10.20f.RestServerValidation.md
@@ -0,0 +1,256 @@
+---
+title: "Jakarta Bean Validation"
+slug: RestServerValidation
+---
+
+**Validation is opt-in and disabled by default.** To enable, mark a
`@Content`, `@FormData`, or `@Request`-bound parameter with
`@jakarta.validation.Valid`. With no marker anywhere on a resource, validation
never runs — no `Validator` is instantiated, no constraint check is invoked,
and no per-request cost is paid, even when `jakarta.validation-api` and a
concrete provider (e.g. Hibernate Validator) are sitting on the classpath.
+
+This is a deliberate departure from Spring MVC, where `@Valid` is implied for
`@RequestBody` and the framework wires up a `Validator` at startup whether the
application has constraints or not. Juneau's contract is the opposite: zero
classpath cost, zero per-request cost, and zero startup cost until a parameter
explicitly opts in.
+
+## Motivation
+
+Juneau's REST layer needs to interoperate with the rest of the Jakarta EE /
Spring ecosystem, where [Jakarta Bean Validation
3.x](https://beanvalidation.org/) is the de facto declarative validation
standard. The contract surfaces as constraint annotations on bean fields:
+
+```java
+public class OrderRequest {
+ @NotBlank String customerId;
+ @NotNull @Positive Integer quantity;
+ @Email String contactEmail;
+}
+```
+
+…and an opt-in marker on the receiving parameter:
+
+```java
+@RestPost("/orders")
+public Order create(@Content @Valid OrderRequest in) {
+ // If any constraint fails, the handler is never called.
+ // Response is 400 with the violations.
+ return orderService.create(in);
+}
+```
+
+Out of the box (no `@Valid`), the bean is bound and the handler runs
regardless of whether its fields satisfy the constraints — the annotations on
the bean class are inert decoration as far as Juneau is concerned. Adding
`@Valid` flips the dispatch: after binding completes, the integration invokes
`validator.validate(bean)`, collects the violations, and short-circuits to a
`400 Bad Request` response if any are reported.
+
+## The off-by-default contract
+
+The off-by-default contract has six tangible guarantees:
+
+1. **Cold-start default.** A fresh `RestContext` built with no
validation-related markers anywhere on the resource never instantiates a
`Validator` and never invokes a constraint check. A request bean with
`@NotNull` / `@Size` / etc. on its fields passes through the unmodified Juneau
pipeline as if those annotations weren't there.
+2. **Per-parameter granularity.** `@Valid` on one parameter never opts in a
sibling parameter, an unrelated handler, or the resource as a whole. The
integration uses a per-parameter cached `boolean validate` flag computed once
at arg-resolver construction time.
+3. **No global "on" switch.** There is no
`RestContext.Builder.validate(true)`, no `juneau.validation.enabled=true`
system property, and no resource-level boolean attribute that enables
validation across every operation in a class. Granular per-parameter opt-in is
the only opt-in surface.
+4. **No silent enablement via classpath presence.** Even if
`jakarta.validation-api` and `hibernate-validator` are on the classpath,
validation does not run unless the user has opted in via `@Valid` (or one of
the equivalent FQN markers — see below).
+5. **Graceful degradation.** If `@Valid` is present but no Jakarta Validation
provider is reachable at runtime, the integration silently skips validation,
emits a one-shot `WARNING` log per JVM, and lets the request continue with the
bean unchanged. The off-by-default behavior is preserved even in misconfigured
deployments.
+6. **Tests verify the contract.** `RestValidation_OffByDefault_Test` exercises
the "violating bean, no `@Valid` marker → reaches handler" and "violating bean,
`@Valid` marker → blocked with 400" pair against the same `@Content` parameter,
against per-method granularity, and against cascading `@Valid` on a nested
property. None of those tests rely on classpath inspection.
+
+## Opt-in marker
+
+The integration recognises three opt-in markers, all detected by
fully-qualified name (FQN) so no compile-time dependency on the marker packages
is required:
+
+| Marker | Source | Notes |
+|---|---|---|
+| `@jakarta.validation.Valid` | Jakarta Bean Validation 3.x | Recommended.
Standard marker for cascading and root-bean validation. |
+| `@javax.validation.Valid` | Bean Validation 2.x (legacy `javax.*` namespace)
| Recognised for migration friendliness. Most modern stacks have moved to
`jakarta.*`. |
+| `@org.springframework.validation.annotation.Validated` | Spring Framework |
Recognised for migration friendliness. Group support is not yet honored — the
marker triggers default-group validation only. |
+
+The marker can sit on a `@Content`, `@FormData`, or `@Request`-bound parameter:
+
+```java
+@RestPost("/orders")
+public Order create(@Content @Valid OrderRequest in) { ... }
+
+@RestPost("/signup")
+public Result signup(@Request @Valid SignupRequest req) { ... }
+
+@RestPost("/upload")
+public Result upload(@FormData(name="*") @Valid UploadMetadata meta) { ... }
+```
+
+## Failure response shape
+
+When validation fails, the integration throws a `ValidationException`
(`BadRequest` subclass, status 400) carrying an immutable
`List<ValidationViolation>`. The response shape depends on whether RFC 7807
problem details are enabled for the operation (see [RFC 7807 / 9457 Problem
Details](/docs/topics/RestServerProblemDetails)).
+
+### Without `problemDetails`
+
+`Content-Type: application/json`:
+
+```json
+{
+ "status": 400,
+ "errors": [
+ { "path": "customerId", "message": "must not be blank", "constraint":
"NotBlank" },
+ { "path": "quantity", "message": "must be greater than 0", "constraint":
"Positive" }
+ ]
+}
+```
+
+The top-level shape mirrors Spring MVC's `MethodArgumentNotValidException`
payload for migration friendliness — `status` and `errors[]` at the root, one
violation per array element.
+
+### With `@Rest(problemDetails="true")`
+
+`Content-Type: application/problem+json`:
+
+```json
+{
+ "type": null,
+ "title": "Bad Request",
+ "status": 400,
+ "detail": "1 violation(s) found.",
+ "errors": [
+ { "path": "customerId", "message": "must not be blank", "constraint":
"NotBlank" }
+ ]
+}
+```
+
+The `errors[]` array is an extension member (RFC 7807 §3.2) — the canonical
members (`type`, `title`, `status`, `detail`) describe the failure category and
the extension carries the per-violation breakdown.
+
+### `invalidValue` privacy
+
+`ValidationViolation` carries an optional `invalidValue` field which is
**omitted by default** for privacy reasons (the offending value may be PII, an
API key, a partial credit card number, or otherwise sensitive). Handlers that
need to surface the value — e.g. an internal debugging endpoint — can include
it explicitly by constructing the violation with the value setter. The renderer
honors a non-`null` value verbatim and omits a `null` one.
+
+## Dependency stance — optional, `provided` scope
+
+`juneau-rest-server`'s `pom.xml` declares the Jakarta Validation API in
`provided` scope:
+
+```xml
+<dependency>
+ <groupId>jakarta.validation</groupId>
+ <artifactId>jakarta.validation-api</artifactId>
+ <version>3.0.2</version>
+ <scope>provided</scope>
+</dependency>
+```
+
+Consumers who never write `@Valid` pay no transitive cost — no Jakarta
Validation, no Hibernate Validator, no Jakarta EL pulled in automatically.
Consumers who opt in supply the runtime provider on their own classpath:
+
+```xml
+<!-- in the consumer's pom.xml -->
+<dependency>
+ <groupId>org.hibernate.validator</groupId>
+ <artifactId>hibernate-validator</artifactId>
+ <version>8.0.3.Final</version>
+</dependency>
+<dependency>
+ <groupId>org.glassfish.expressly</groupId>
+ <artifactId>expressly</artifactId>
+ <version>5.0.0</version>
+</dependency>
+```
+
+Hibernate Validator is the reference implementation of Bean Validation 3.0 and
is the recommended provider. Jakarta Expression Language (Expressly) is
required for dynamic message interpolation — without it, Hibernate Validator
throws on the first `.validate()` call with `HV000183`.
+
+## Graceful degradation
+
+If a parameter carries `@Valid` but no Jakarta Validation provider is
reachable at runtime (the API JAR is on the classpath but the engine isn't),
the integration silently skips validation and emits a one-shot `WARNING` log
per JVM:
+
+```text
+WARNING: Jakarta Bean Validation was requested via @Valid on a REST argument,
+but no jakarta.validation.Validator bean is registered and no runtime provider
+(e.g. org.hibernate.validator:hibernate-validator + jakarta.el implementation)
+is on the classpath. Validation will be silently skipped for this and
+subsequent requests. Add a provider dependency or register a Validator bean
+to enable validation.
+```
+
+The request continues through the handler with the bean unmodified — the
off-by-default contract is preserved. This matches the FQN-detection precedent
established by JSR-330 / Spring-Lite (`FINISHED-24`): feature presence is
signaled by the consumer (via the marker annotation) but the engine is a
runtime resolution that fails open, not a startup-time hard requirement.
+
+## `Validator` resolution order
+
+The integration consults, in order:
+
+1. **A user-supplied `jakarta.validation.Validator` bean** visible from the
resource's `BeanStore`. Typically contributed via a `@Bean` factory on the
resource class:
+
+ ```java
+ @Rest(path="/orders")
+ public class OrderResource {
+ @Bean public Validator validator() {
+ return Validation.byDefaultProvider()
+ .configure()
+ .messageInterpolator(new ParameterMessageInterpolator()) // no
jakarta.el
+ .buildValidatorFactory()
+ .getValidator();
+ }
+ }
+ ```
+
+ Use this when the deployment needs custom message interpolators, group
sequences, a non-default `ConstraintValidatorFactory`, or a parameter-message
interpolator that doesn't require jakarta.el at runtime.
+
+2. **A lazily-built JVM-wide default** obtained from
`Validation.buildDefaultValidatorFactory()`. Cached after the first successful
build so the factory cost is paid at most once per JVM, regardless of how many
`@Rest` resources opt in.
+
+3. **`null`** — the graceful-degradation path described above.
+
+## Custom constraints
+
+User-defined constraints work transparently — the integration delegates
entirely to the underlying `jakarta.validation` engine for constraint discovery:
+
+```java
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = SkuValidator.class)
+public @interface Sku {
+ String message() default "must match SKU-NNNN format";
+ Class<?>[] groups() default {};
+ Class<? extends Payload>[] payload() default {};
+}
+
+public class SkuValidator implements ConstraintValidator<Sku, String> {
+ public boolean isValid(String value, ConstraintValidatorContext ctx) {
+ return value != null && value.matches("SKU-\\d{4}");
+ }
+}
+
+public class OrderItem {
+ @Sku public String sku;
+ @Min(1) public int qty;
+}
+```
+
+A `@Valid OrderItem` parameter will surface a `Sku`-named constraint in
`errors[].constraint` alongside the built-in `Min` from
`jakarta.validation.constraints.Min`.
+
+## Nested / cascading validation
+
+`@Valid` on a nested property tells the validator to recurse into that
property and re-evaluate its constraints — but only if the parent's `@Valid`
has triggered validation in the first place. The cascade still respects the
off-by-default contract: if the parent has no `@Valid`, the nested `@Valid`
alone does not trigger validation.
+
+```java
+public class Address {
+ @Pattern(regexp="\\d{5}") public String zip;
+}
+
+public class Customer {
+ @NotBlank public String name;
+ @Valid public List<Address> addresses; // cascades only if Customer
has @Valid above
+}
+
+@RestPost("/customer")
+public String create(@Valid @Content Customer c) {
+ return "ok:" + c.name;
+}
+```
+
+A bad zip surfaces at path `addresses[0].zip` in the violation list, proving
the recursion happened.
+
+## API surface
+
+| Class | Package | Notes |
+|---|---|---|
+| `ValidationException` | `org.apache.juneau.rest.validation` | `BadRequest`
(400) subclass. Carries `List<ValidationViolation>`. |
+| `ValidationViolation` | `org.apache.juneau.rest.validation` | `path`,
`message`, `constraint`, optional `invalidValue`. |
+| `BeanValidator` | `org.apache.juneau.rest.validation` | Static dispatcher.
`isValidationRequested(ParameterInfo)`, `validate(T bean, BeanStore)`. |
+
+The arg-resolver wiring lives in `ContentArg`, `FormDataArg`, and
`RequestBeanArg` — each computes its per-parameter `boolean validate` flag once
at construction time and conditionally invokes `BeanValidator.validate(...)`
per request only when the flag is `true`.
+
+## What's out of scope (v1)
+
+The following are explicit non-goals for the initial integration. Each can be
added in a follow-on release without breaking the v1 contract.
+
+- **Method-level validation.** Constraints on `@RestOp` return values
(`@Valid` on the return type) and `@Validated` on the resource class (which
would imply method-parameter validation across every operation in the class).
The v1 integration validates parameter beans only.
+- **Schema-driven validation.** JSON-Schema constraints on the request body —
that lives in `JsonSchemaGenerator` territory.
+- **Group support.** `@Validated(Group.class)` recognition for selective group
validation. The marker is detected but the group classes are not yet passed
through to `validator.validate(bean, groups...)`.
+- **`Messages.properties` bridge.** Cross-bundle composition between Juneau's
`Messages` and Jakarta's `ValidationMessages.properties`. The two bundles don't
compose automatically; document and configure separately.
+- **Per-resource / per-method `validate=true` switch.** A coarse-grained
opt-in attribute on `@Rest` and `@RestOp` that enables validation for every
parameter on the targeted scope without per-parameter `@Valid` markers. The v1
integration is `@Valid`-only on the parameter.
+
+## See also
+
+- [RFC 7807 / 9457 Problem Details](/docs/topics/RestServerProblemDetails) —
the problem-details response shape that the validation integration plugs into
for the `application/problem+json` path.
+- [Bean Validation 3.0
specification](https://jakarta.ee/specifications/bean-validation/3.0/) — the
underlying Jakarta API.
+- [Hibernate Validator 8.0
reference](https://hibernate.org/validator/documentation/) — the recommended
provider.
diff --git a/sidebars.ts b/sidebars.ts
index 07a29cea72..0eb3832853 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1551,6 +1551,11 @@ const sidebars: SidebarsConfig = {
id:
'topics/10.20e.RestServerAuthGuards',
label: '10.20e. AuthN
Guards — Bearer / API-Key / JWT',
},
+ {
+ type: 'doc',
+ id:
'topics/10.20f.RestServerValidation',
+ label: '10.20f. Jakarta
Bean Validation',
+ },
{
type: 'doc',
id:
'topics/10.21.BuiltInParameters',