REST OpenAPI validation
View MarkdownStatus: Stable Package:
dev.vertique.rest.openapi.validationArtifact:vertique-rest-openapi-validationDepends on: rest-jaxrs, json-schema-validation API reference: javadoc.io
Opt-in openapi-contract request-validation strategy that validates incoming requests against the generated openapi.json using vertx-openapi. This module is the only place in the framework that takes a production/runtime dependency on io.vertx:vertx-openapi; the default web-validation path (in vertique-rest-validation) carries no such dependency. (vertx-web-openapi-router appears as a test-scope dependency in vertique-rest-security and vertique-rest-auth-jwt for legacy IT support, but those are test-only and do not affect the production classpath.) Applications install this module only when spec-strict contract validation and the vertx-openapi preview artifact are both acceptable.
When To Use It
Section titled “When To Use It”Install OpenApiContractValidationModule when:
- Contract-driven validation (types, patterns, enums) from the generated spec is required
- The application can accept the
vertx-openapipreview artifact on the runtime classpath
For most new applications, the default web-validation strategy (annotation-synthesized schemas via vertique-rest-validation) covers the same constraints without the preview artifact. Use this module only when a pre-existing spec is the authoritative source and annotation parity is insufficient.
Core Concepts
Section titled “Core Concepts”The openapi-contract strategy loads an OpenAPIContract asynchronously, caches the contract and the
standalone Vert.x validator derived from it (io.vertx.openapi.validation.RequestValidator — a
vertx-openapi type, not a Vertique one), and returns a contract-backed gate for every operation.
Validation strictness is therefore whatever the Vert.x validator enforces against the contract; the
framework adds no schema layer of its own. jaxrs.validationMode belongs to web-validation; this
strategy uses the standalone validator’s result directly.
OpenApiContractValidationStrategy is a Dagger singleton, but the contract it validates against is
resolved per mount: it keeps one loaded contract plus validator per distinct
MountMeta.openapiPath(). Mounts declaring different openapiPath values are supported and each
validates against its own contract; mounts sharing one path share one load. The global
jaxrs.openapiPath is loaded (“pre-warmed”) when the singleton is constructed, so the usual
single-mount application still starts its contract load as early as possible. The pre-warm itself is
not awaited: when no mount binds the global path, a failure to load it is only logged as a WARN and
does not fail startup. When a mount does bind it, that mount’s startup check awaits it like any other
mount contract.
Per-mount contract binding (bindToMount). Before any of a mount’s operation gates are produced,
the framework calls bindToMount(MountMeta) once for that mount. This implementation caches the
mount’s contract under mountMeta.openapiPath(), starting the load only the first time a given path
is seen (computeIfAbsent), so binding is idempotent and safe when several mounts — or several
HttpVerticle instances — bind concurrently. A mount declaring no openapiPath is rejected with
RestConfigurationException naming the mount id, because this strategy cannot validate without a
contract. A contract that fails to load is logged as a WARN naming the mount and the path, and its
failure is cached and never retried; the startup contract-load check below turns it into a startup
failure for every mount bound to that path.
Startup contract-load check. The module also contributes a contract-load check that runs when each
JAX-RS mount built under openapi-contract and bound by the strategy finishes building its router:
that mount’s router creation waits for the cached load of every contract bound under the mount’s id —
more than one when hand-built mounts sharing a mount path were bound to different contracts. A
contract vertx-openapi cannot load fails the mount’s router, and so startup — no server listens — with a
RestConfigurationException that has no cause. The message names the application and the setting its
contract location came from — jaxrs.applications.<name>.openapiPath,
the @RestApplication annotation's openapiPath, or jaxrs.openapiPath, or a generic phrase when the
application is not in the declared-application view — or, for a hand-built mount serving no declared
application, the mount path.
It then gives exactly one reason:
its location must end in .json, .yaml, or .yml— checked before loading, without regard to case;the file cannot be read;a servers url is not a valid URL; use a valid URL or omit servers— a malformed server URL;the file is not a valid OpenAPI contract— any other contractvertx-openapirejects.
The message never echoes the location, the file’s content, or parser text; the strategy’s WARN log
still carries the underlying cause for operators. The check starts no load of its own: each distinct
path is still loaded once and that load is reused by every gate, with no per-request reload. Mounts
under any other strategy (web-validation, none) and empty mounts are untouched. Configurations
that used to start and then answer every validated request with HTTP 500 now fail at startup instead.
A multi-mount application whose mounts use contracts other than the default openapi.json should set
jaxrs.openapiPath to one of its mount contract paths (or to null) so the construction pre-warm
does not log a spurious WARN for a contract no mount uses.
Gate lifecycle (gateFor). After mount binding, JaxRsRouteRegistrar calls
gateFor(JaxRsOperationDescriptor, OperationSchemas, MountMeta) once per operation at router-build
time, and the gate closes over that mount’s contract. This strategy ignores the synthesized schemas
and always returns a handler. At request time the handler looks up the exact operationId, extracts a
ValidatableRequest from the body already buffered by BodyHandler, and validates it against the
mount’s cached contract. A missing operationId is a server/configuration failure: it is logged with
the mount path and the contract path, and reaches the REST pipeline as HTTP 500. A contract that
failed to load reaches a gate only when the strategy is driven directly, without this module’s
startup check; it then fails the same way, as HTTP 500. Validator request failures become sanitized
HTTP 400 responses. The gate reads an
already-loaded contract synchronously and continues the request on the request’s own Vert.x context;
a still-loading contract is awaited and the continuation is dispatched back onto that context, so a
request is never continued on the event loop that loaded the contract.
The two-argument gateFor(JaxRsOperationDescriptor, OperationSchemas) is a legacy form the framework
no longer calls. It validates against the global jaxrs.openapiPath contract and fails closed with an
IllegalStateException — naming the bound contract paths — once any mount with a different
openapiPath is bound, rather than validating that mount against the wrong contract.
openapi-contract does not execute FileContentVerifier bindings and inherits
runsFileVerifiers() == false. When such verifiers are bound, JaxRsRouterMount emits one startup
WARN for each mount that selects this strategy; @FilePart and verifier execution are provided by
web-validation only.
Error sanitization. Validation errors produced by the openapi-contract strategy are sanitized before reaching the client: submitted values and the validator’s raw error text are stripped. Each entry carries only the violation location in pointer (an RFC 6901 JSON Pointer, percent-decoded), the failed keyword as type, and a stable detail message. For an additionalProperties violation the undeclared key is cut back to its parent, so that key is never echoed. Other client-chosen member names are not removed: a key of a map in the body, a patternProperties, propertyNames, unevaluatedProperties or schema-valued additionalProperties member (the strategy cuts only an additionalProperties key), and the key of an object-valued parameter (deepObject or form-exploded query object) can all appear in pointer. A segment holding a control character or a line or paragraph separator ends the pointer before that segment. A control character below 0x10 in a client key can appear as the engine’s escape text (for example %A for a line feed), which is not decoded. Every entry is labelled location: "body", including one for a parameter schema violation, and none carries parameter.
JSON profile first-parse. When a route’s resolved JSON mapper is not the same instance as the process codec’s mapper (see dev.vertique:vertique-rest-jaxrs → request-body profiles, whose resolver returns that identity sentinel), the openapi-contract strategy runs that profile mapper’s first parse of the request body — applying its strict parser features and rejecting a non-conforming body with a 400 — before OpenAPI schema validation, consistent with the default web-validation strategy. No profile is stashed when the route’s mapper is the process codec’s own instance, in which case this is a no-op and OpenAPI validation runs unchanged.
Key Classes
Section titled “Key Classes”OpenApiContractValidationModule
Section titled “OpenApiContractValidationModule”Dagger @Module and the module’s wiring entry point. Contributes
OpenApiContractValidationStrategy to the Set<RequestValidationStrategy> multibinding declared by
RestModule, and the startup contract-load check (see Core Concepts) to
vertique-rest-jaxrs’s Set<MountPublicationHook> multibinding. The check is internal; nothing
in an application calls or replaces it.
@Modulepublic abstract class OpenApiContractValidationModule { ... }Include alongside RestModule to make the openapi-contract strategy available:
@Singleton@Component(modules = { VertxModule.class, RestModule.class, OpenApiContractValidationModule.class, // contributes openapi-contract strategy AppModule.class, ResourceModule.class})interface AppComponent { HttpVerticle httpVerticle();}Then activate it in config:
{ "jaxrs": { "validationStrategy": "openapi-contract", "openapiPath": "openapi.json" }}OpenApiContractValidationStrategy
Section titled “OpenApiContractValidationStrategy”RequestValidationStrategy implementation that loads an OpenAPIContract per mount openapiPath and
produces Vert.x contract-backed validation gates per operation.
public class OpenApiContractValidationStrategy implements RequestValidationStrategy { @Override public String id() { return "openapi-contract"; }
// Caches this mount's contract; rejects a mount that declares no openapiPath. @Override public void bindToMount(MountMeta mountMeta) { ... }
// The form the framework calls: validates against the registering mount's own contract. @Override public Optional<Handler<RoutingContext>> gateFor( JaxRsOperationDescriptor operation, OperationSchemas schemas, MountMeta mount) { ... }
// Legacy, mount-agnostic form: the global contract only, fails closed on divergent mounts. @Override public Optional<Handler<RoutingContext>> gateFor( JaxRsOperationDescriptor operation, OperationSchemas schemas) { ... }}Each distinct contract path is loaded once — at construction for the global jaxrs.openapiPath, at
bindToMount for a mount’s own path — and that one load is reused by every gate built for it. A
missing or malformed contract fails the cached future and is not retried; in a composition with this
module, the startup contract-load check fails the router of every mount bound to that path, and so
startup. Driven directly, without that check, the failure surfaces as HTTP 500 when a request reaches
one of that mount’s gates, while other mounts keep working.
The strategy still passes the framework’s ParamConversionResolver (vertique-rest-core) into the DefaultBoundRequest it constructs. Constructing that binder triggers the JSON-profile first-parse of the body. The binder stores declared scalars as raw strings and does not coerce them. ParameterExtractor converts after input policies, through the same resolver as dispatch.
Invariants and Gotchas
Section titled “Invariants and Gotchas”openapiPathmust resolve through Vert.x file-system loading. A missing, unreadable, or malformed spec, or one whose location does not end in.json,.yaml, or.yml, fails startup through the contract-load check with a cause-freeRestConfigurationExceptionnaming the application’s contract setting (or the mount path) and the reason, never the location or content. The load is cached rather than retried, and the framework does not fall back silently.- A malformed
serversURL fails startup.vertx-openapiloads a contract whoseserversare absolute, relative such as/api, or absent, but not one holding a URL that cannot be parsed, and startup fails with the reasona servers url is not a valid URL; use a valid URL or omit servers. Theserversbase path plays no part in validation: the gate matches by operation id and reads path parameters from the request path, so a relativeserversURL changes nothing at request time. - An unloadable global contract no mount uses does not fail startup. The construction pre-warm of
jaxrs.openapiPathis not awaited; when no mount binds that path, its failure is only a WARN. - Every mount must declare an
openapiPath. Binding a mount without one throwsRestConfigurationExceptionat startup naming the mount id; the strategy never borrows another mount’s contract. - Each declared application resolves its own contract location.
vertique-rest-jaxrs’sRestApplicationsview decides each application’s effectiveopenapiPathby precedence (jaxrs.applications.<name>.openapiPath, then the@RestApplicationannotation’sopenapiPath, then the globaljaxrs.openapiPath); this strategy then caches and validates against one loaded contract per distinctopenapiPathvalue, so two applications resolving to the same location share one contract and two resolving to different locations validate against different contracts. An operationId therefore only has to exist in the contract of the mount that serves it — but two operations that share an operationId across different JAX-RS mounts are still refused at startup byvertique-rest-jaxrs’s mount composition validator (its global cross-mount operationId rule, same owner exempted), even when both mounts resolve to the same contract location, until a later change scopes that rule per mount. - This strategy reports
resolvesOperationsFromMountContract()(true). The rest-jaxrs mount composition validator reads that flag from the strategy selected byjaxrs.validationStrategy, but only once an application is declared (active or not) or an application mount is present; under that gate, when the flag is set, it normalizes every JAX-RS mount’s contract location and reports one it cannot parse as a startup violation, naming the mount and never the value. - operationId matching is exact. A route whose operationId is absent from its mount’s contract
fails requests with HTTP 500 and logs an
ERRORnaming the mount path and the contract path; it never falls back to an unvalidated route. - File verification is inactive.
openapi-contractdoes not run@FilePartconstraints orFileContentVerifier; bound verifiers cause a per-mount startup WARN. - Known limit: pattern and format input is not bounded. The pattern-input bound
(
jaxrs.validationPatternMaxCharsandjaxrs.validationPatternMaxTotalChars) and the reused format checks belong to theweb-validationgate only; those keys are scoped toweb-validation, and this strategy does not consult them. Underopenapi-contract, a string or object key reaching apattern,patternProperties, or format position is handed tovertx-openapiat whatever length the request carries, and is evaluated by that library’s own keyword and format handling. An application that needs the bound selectsweb-validation. location: "body"does not mean a body location here. This strategy labels every entrybody, including a violation of a parameter schema, and never setsparameter. Itspointeris percent-decoded and can carry client-chosen member names (see Error sanitization), includingpropertyNamesandunevaluatedPropertiesmembers and a control character below0x10as the engine’s escape text, so a consumer that renders or logs it must encode it for that sink; a pointer never holds a control character, but it can hold markup, quotes or whitespace.vertx-openapiis a preview artifact. Its API shape may change across Vert.x minor versions. This module pins thevertx-openapiversion via the parent BOM.- Security semantics. The active security model is OR-of-AND-with-scopes. The
openapi-contractstrategy inherits the same security handling as all other strategies — security is applied byJaxRsRouteRegistrar, not by the validation strategy itself. The validation gate runs after the auth/authorization chain and is unaffected by the security model shape.
Module Dagger Bindings
Section titled “Module Dagger Bindings”OpenApiContractValidationModule contributes:
| Binding | Value |
|---|---|
RequestValidationStrategy |
@Binds @IntoSet → OpenApiContractValidationStrategy (id openapi-contract) |
MountPublicationHook |
@Binds @IntoSet → startup contract-load check (INTERNAL; sibling-framework seam) |
The strategy is selected by jaxrs.validationStrategy: openapi-contract. The publication hook is
not an application SPI.
Dependencies
Section titled “Dependencies”dev.vertique:vertique-rest-jaxrs(forRequestValidationStrategy, descriptors, config, and REST error types)dev.vertique:vertique-json-schema-validation(forViolationDetails.rfc6901, which turns the validator’s instance location into the reportedpointer)io.vertx:vertx-openapicom.google.dagger:daggerjakarta.inject:jakarta.inject-api