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',

Reply via email to