REST OpenAPI docs
View MarkdownStatus: Beta Package:
dev.vertique.rest.openapi.docs(ApiDocsandOpenApiDocsModule; its subpackages are internal to the module) Artifact:vertique-rest-openapi-docsDepends on: rest-jaxrs, rest-core, security-core, core, json-schema API reference: javadoc.io
Opt-in runtime OpenAPI 3.1 documents for named REST applications. An application interface that
carries @ApiDocs gets two read-only documents, openapi.json and openapi.yaml, served under a
server-level prefix (default /apidocs) at <prefix>/<application name>/openapi.json and
<prefix>/<application name>/openapi.yaml. Each document is assembled once per application at
startup, on a worker thread, and served from frozen bytes with strong entity tags, conditional
304 answers, HEAD, and Cache-Control.
A document describes the application’s inputs and responses: openapi (3.1.1), the info object
from configuration or from @OpenAPIDefinition(info), the JSON Schema dialect, one server, every
visible operation of the mount with its parameters, request body, and responses, the component
schemas those inputs and responses reference, the tags its operations declare, and the pattern
dialect (see Document Content). Responses are inferred from each resource
method’s return type or taken from its @ApiResponse declarations (see
Operation Responses). Operations, parameters, request bodies, and responses
carry the documentation of their Swagger annotations (see
Swagger annotations). Each operation lists the security requirements it
declares, and components.securitySchemes describes exactly the schemes they reference (see
Operation Security).
An application that names its own OpenAPI contract (@RestApplication.openapiPath or
jaxrs.applications.<name>.openapiPath) gets no generated document: its contract is checked against
its routes at startup and served as parsed (see Served Contracts).
The module serves documents only. It has no UI, no assets, and no other route, and @ApiDocs is not
API protection: it does not change who may call any operation of the application.
When To Use It
Section titled “When To Use It”Add dev.vertique.rest.openapi.docs.OpenApiDocsModule to the Dagger @Component of an application
that declares one or more @RestApplication interfaces (the annotation comes from
dev.vertique:vertique-rest-core; dev.vertique:vertique-rest-jaxrs composes the applications)
and wants a machine-readable OpenAPI document for some of them.
Three things switch a document on; all three are required:
OpenApiDocsModuleis listed in the component.- The application’s declaring interface carries
@ApiDocs, whosepolicyis anAccessPolicythat states who may read the document: a policy that is exactly one@PermitAllfor a document any caller may read, or any other policy with asecurityScheme(see Protected Documents). - The document has an
infowith a non-blanktitleandversion, fromapidocs.documents.<name>.infoor from@OpenAPIDefinition(info)on the declaring interface, where<name>is the application’sname(see Configuration). This holds for a generated document only: an application that serves its own contract takes itsinfofrom that contract, and a configuredinfois refused (see Served Contracts).
@ApiDocs(policy = PublicApi.DocsPolicy.class)@RestApplication(name = "public", path = "/api/public", resources = {CatalogResource.class})public interface PublicApi {
@PermitAll public interface DocsPolicy extends AccessPolicy {}}{ "apidocs": { "documents": { "public": { "info": { "title": "Catalog", "version": "1.0" } } } }}The document is then served at /apidocs/public/openapi.json and /apidocs/public/openapi.yaml.
The component also needs dev.vertique.config.parser.ConfigParsingModule from
dev.vertique:vertique-config-core, which every Vertique application already lists: the module
parses the apidocs section through the canonical ConfigParser.
No document exists for:
- the legacy default mount (the one served without any
@RestApplication); - a manually built
JaxRsRouterMount; - an application whose registration is inactive (for example, switched off by configuration);
- an application whose declaring interface has no
@ApiDocs. Anapidocs.documents.<name>entry for it is accepted and publishes nothing, unless it setsenabled: true, which fails startup (see Configuration).
Core Concepts
Section titled “Core Concepts”One document per documented application
Section titled “One document per documented application”A document is named by its application’s name, not by its mount path. The set of documents is
decided once per component from the declared applications that vertique-rest-jaxrs reports as
active. A document is enabled exactly when all of these hold:
apidocs.enabledis notfalse;- the application is active;
- the application’s declaring interface itself carries
@ApiDocs(a superinterface’s@ApiDocsis not consulted); apidocs.documents.<name>.enabledis absent,null, ortrue.
Nothing else enables a document: not the module’s presence, not another annotation, and not a configuration entry alone.
Access: PUBLIC and PROTECTED
Section titled “Access: PUBLIC and PROTECTED”@ApiDocs.policy states who may read the document routes: a public interface that extends
AccessPolicy (from vertique-security-core) and declares its requirements with the annotations a
resource method uses, @PermitAll, @DenyAll, @RolesAllowed, @Authorized, and @RequiresAction.
Only the annotation on the declaring interface decides it; no configuration value changes it.
The document’s classification is derived from the policy and is never declared. It is exactly one of two values:
PUBLIC: the policy is exactly one@PermitAll. The document is served to any caller, without authentication.PROTECTED: every other valid policy,@DenyAllincluded. The document is served through the security chain of an equally annotated resource method: the authentication handler of@ApiDocs.securityScheme, then every registeredOperationHandlerContributorwith the requirements of the policy. A policy that denies everyone authenticates the reader, then refuses it, so in a supported composition no reader gets the document; the document’s own handler also refuses every request of a lone@DenyAllpolicy and never reads the store for it. See Protected Documents.
The shape check refuses a policy that is not a valid AccessPolicy, a securityScheme on a public
document, even a blank one, and a missing or blank securityScheme on any other policy (see
@ApiDocs shape).
Built once, off the event loop, frozen
Section titled “Built once, off the event loop, frozen”When an application’s mount is built at startup, the module receives the mount’s publication (the operations with their resolved inputs, schemas, and response shape), copies what it needs during that call, and assembles the document on a worker thread. The composition that assembles a document is the only one that generates its response schemas (see Output profile and generation). The document is stored as immutable JSON and YAML bytes built from one tree, together with a strong entity tag for each form. Nothing is assembled per request, no schema source or schema generator is called per request, and no event loop waits for another. Assembly has no time bound: an assembly that never returns stalls startup (see Loaded once for a served contract).
- JSON: compact UTF-8 with the root members in a fixed order (see
Document Content);
infoholds the configuredtitle,description, andversion, or the members of the annotatedinfo(seeinfo). - YAML: written from the same tree with Jackson’s default YAML settings, so the parsed YAML tree equals the JSON tree.
- Identical inputs produce identical bytes and identical entity tags.
For the example above, with no serverUrl configured, no request-validation gate installed, and a
CatalogResource at @Path("/items") declaring String listItems(@QueryParam("limit") Integer limit) on GET and String getItem(@PathParam("id") String id) on GET /{id}, both with
@Produces(TEXT_PLAIN), and void createItem(@QueryParam("dryRun") boolean dryRun, CreateItemRequest request) on POST with @Consumes(APPLICATION_JSON), where CreateItemRequest
is record CreateItemRequest(String name, int quantity), the JSON document is exactly (wrapped here
for reading):
{"openapi":"3.1.1","info":{"title":"Catalog","version":"1.0"},"jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","servers":[{"url":"/api/public"}],"paths":{"/items":{"get":{"operationId":"listItems","parameters":[{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"text/plain":{}}}}},"post":{"operationId":"createItem","parameters":[{"name":"dryRun","in":"query","schema":{"type":"boolean"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/createItem.request"}}}},"responses":{"204":{"description":"No Content"}}}},"/items/{id}":{"get":{"operationId":"getItem","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"text/plain":{}}}}}}},"components":{"schemas":{"createItem.request":{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"name":{"type":"string"},"quantity":{"type":"integer"}},"type":"object"}}},"x-vertique-validation":{"patternDialect":"java.util.regex"}}The served bytes have no line breaks. limit and dryRun carry no required member because
neither is certainly required, and the request body carries none because no validation gate is
installed. The two String operations publish raw text without a schema, and the void operation
publishes 204 (see Operation Responses).
The docs mount
Section titled “The docs mount”All documents are served by one router mount at <apidocs.path>/*. It runs in the
SYSTEM_FIRST phase (ordered among other SYSTEM_FIRST mounts by priority, then path), and carries
the fixed mount metadata
new MountMeta("apidocs", "<path>/*", null, Set.of()), so a MountCustomizer sees it like any other
mount and applies to it unless its matches method filters it out. The module adds no filtering (see
Mount Customizers). A component builds one docs mount per composition, and the
mount refuses to create its router unless the module’s composition validator has checked the
composition (see Startup Checks).
What the mount answers, and what it passes on
Section titled “What the mount answers, and what it passes on”The mount answers GET and HEAD for exactly <path>/<name>/openapi.json and
<path>/<name>/openapi.yaml of each enabled document. The query string is ignored. These requests
continue to the later mounts unchanged, for every document:
- another HTTP method on a document URL;
- an unknown document name, or the name of an application without an enabled document;
- any other URL under the prefix.
For a public document, the mount also calls next() for:
- a trailing-slash variant of a document URL;
- a document whose bytes are not stored yet.
For a public document, the mount itself never answers 404 or 405. Whatever the later mounts
answer, including a catch-all mount, is the response. When no later mount claims the request, the
router answers it. A request whose path matches a document URL or its trailing-slash variant, for
any document, public or protected, but whose method is not GET or HEAD, such as
POST /apidocs/public/openapi.json or POST /apidocs/public/openapi.json/, ends with 405 (the
router’s method-mismatch status, whose Allow header names GET and HEAD). An unknown document
name ends with 404.
A protected document’s route never continues to a later mount. Once its security chain has passed,
a trailing-slash variant ends with 404 and a document that is not stored yet ends with 503;
a denial ends with 401 or 403 before that (see
Protected Documents).
Responses
Section titled “Responses”| Situation | Response |
|---|---|
GET of a stored document |
200, Content-Type: application/json or application/yaml, Content-Length, ETag, Cache-Control, and the document bytes |
HEAD of a stored document |
200 with the same headers as GET and no body |
GET or HEAD with a matching If-None-Match |
304 with ETag and Cache-Control and no body |
A protected document’s 200 and 304 also carry Vary when its scheme has a credential header
(see Caching), and its 401, 403, 404, 503, and 500 answers are problem
responses (see Protected Documents).
- Entity tag: strong, a double quote, the lowercase hexadecimal SHA-256 of that form’s bytes, and a double quote. The JSON and YAML forms have different tags.
If-None-Match: each header value is split on commas and each member trimmed. A member matches when it is*or equals the tag after a leadingW/is dropped (weak comparison). Any other value returns200.- Fresh buffers: each response sends the frozen bytes in a new buffer, so no response can alter the stored document.
Caching
Section titled “Caching”The Cache-Control of a document never allows shared caching and is never weaker than the configured
default.
Public documents. The value is computed once at startup from the effective default, which is
the value of the last
Cache-Control header, in map order, among the headers that jaxrs.defaultHeaders resolves to (the
framework default is no-store; see dev.vertique:vertique-rest-core):
no-storewhen that default has ano-storedirective, otherwiseno-cache;- prefixed with
private,when that default has aprivatedirective; - so the value is one of
no-store,no-cache,private, no-store, orprivate, no-cache; and - when no default
Cache-Controlexists,no-cache.
Directive names are compared case-insensitively. The value is never public and carries no
s-maxage. The mount sets the header itself, replacing the default-headers middleware’s value.
Protected documents. Every 200 and 304 carries Cache-Control: private, no-store, whatever
the default, and a Vary on the request header that carries the scheme’s credential, decided at
startup from the scheme handler’s openApiDescription():
| Scheme | Vary |
|---|---|
| HTTP, OAuth 2, or OpenID Connect | Authorization |
| A handler that returns no description, or a description of any other kind | Authorization |
| API key in a header | the API key’s header name |
| API key in a cookie | Cookie |
| API key in the query, or mutual TLS | none |
The document route adds its Vary value to any Vary another handler (such as CORS) already set, so
Vary: Origin is kept next to Vary: Authorization. The problem responses of a protected document
carry Cache-Control: no-store; the document route adds neither ETag nor Vary to a problem response (other handlers, such as CORS, may add their
own headers).
Only Cache-Control is replaced. Other caching headers that jaxrs.defaultHeaders sets, such as
CDN-Cache-Control, Surrogate-Control, X-Accel-Expires, or Expires, are not removed from a
protected document response, nor from a problem response, so a CDN or proxy that obeys them could
store a protected document. Do not configure such headers while a protected document is enabled.
Several server instances
Section titled “Several server instances”A component may deploy several HttpVerticle instances. They share one document per application:
- The first instance to publish an application’s mount assembles and stores the document. Every other instance waits without blocking its event loop, then renders a digest of its own publication, operation by operation, on a worker thread and compares it with the stored one.
- When the publications differ, the later instance’s startup fails naming the application, its declaring interface, its mount path, and the first differing operation id in operation-id order (only the application, interface, and mount when just the mount-level facts differ). The message contains no digest, schema, or value.
- When assembly fails, every instance waiting on it fails with the same failure, the failed flight is discarded, and a later deployment in the same component assembles again.
- Each instance completes its own publication step on its own event loop.
All instances of one component must therefore publish the same operations and schemas for a documented application. An operation source whose answers change between instances, such as a stateful schema source, fails the later instance at startup.
Nothing is built without an enabled document
Section titled “Nothing is built without an enabled document”With no enabled document, because apidocs.enabled is false, no application carries @ApiDocs,
every annotated application is switched off, or no application is registered, the module contributes
no publication hook and no docs mount. No publication is built, no composition validator runs, no
startup warning is logged, and routes, validation, and schema-source calls are exactly as without
the module. In particular, no request body is checked for a redaction manifest, so a schema source
that binds none starts and routes unchanged, and no response is inferred, generated, or checked, so
a renamed or hidden output member or an output type the generator rejects does not affect startup.
No SecuritySchemeHandler is asked for its openApiDescription(), so a handler that describes
nothing starts and guards its routes unchanged.
The same holds for an application whose own document is disabled while another one is enabled.
Only two configuration checks still run when apidocs.enabled is not false: the checks of every apidocs.documents entry and the @ApiDocs
shape check of every active application (see Configuration). With
apidocs.enabled false, nothing runs.
Startup log lines
Section titled “Startup log lines”- One INFO line per stored document, naming the application, its mount, and the document’s source,
generatedorserved contract:The document of application '<name>' at mount '<mount path>' is stored (source: <source>). - One INFO line on logger
dev.vertique.rest.openapi.docs.DocumentWarnings, once per document and component, logged after the document’s assembly or contract load succeeds (a failed one logs none), naming where the document comes from:apidocs.documents.<name>: the document of application '<name>' is generated, or, for a served contract,apidocs.documents.<name>: the document of application '<name>' is served from <resolved location>, where the resolved location is an absolute file path or a classpath resource URL. - DEBUG lines from
dev.vertique.rest.openapi.docsrecord each assembly (application, mount, elapsed milliseconds; no content), each load of a served contract (Loaded the served contract of apidocs.documents.<name> at mount '<mount path>' in <n> ms; no content), and each comparison between instances. - One INFO line per protected document whose policy is exactly one
@Authorizedthat lists no scope, on loggerdev.vertique.rest.openapi.docs.DocumentWarnings, once per document and component, logged after every document route is installed:apidocs.documents.<name>: the protected document is readable by any principal the '<scheme>' handler authenticates; the policy of @ApiDocs on <interface> requires authentication only. The message is one line; it is wrapped here for reading. No other policy logs it.
Getting Started
Section titled “Getting Started”The example examples/vertique-example-apidocs/ publishes one public and one protected document.
The steps below are taken from it; the Developer Guide explains each choice.
1. Add the artifact and list the module. Depend on dev.vertique:vertique-rest-openapi-docs
(its version comes from the framework BOM) and list OpenApiDocsModule beside RestModule and the
generated module that holds the application registrations:
@VertiqueApp@Singleton@Component( modules = { VertxModule.class, ConfigParsingModule.class, RestModule.class, RestValidationModule.class, JwtAuthModule.class, OpenApiDocsModule.class, // … deployment and lifecycle modules … AppModule.class, GeneratedJaxRsResourcesModule.class })interface AppComponent extends VertiqueApplicationComponent {}2. Declare the applications and their document access. Access is code, on the declaring
interface: each @ApiDocs names a policy, an interface that extends AccessPolicy, here nested in
the declaring interface. The public document takes its info from @OpenAPIDefinition:
@RestApplication( name = "public", path = "/api/public", resources = {CatalogResource.class})@ApiDocs(policy = PublicApi.DocsPolicy.class)@OpenAPIDefinition(info = @Info(title = "Catalog API", version = "1.0"))public interface PublicApi {
@PermitAll public interface DocsPolicy extends AccessPolicy {}}
@RestApplication( name = "management", path = "/api/mgmt", resources = {ManagementResource.class})@ApiDocs(policy = ManagementApi.DocsPolicy.class, securityScheme = "bearerAuth")public interface ManagementApi {
@RolesAllowed("admin") public interface DocsPolicy extends AccessPolicy {}}A policy states the audience with the annotations a resource method uses: @PermitAll for a public
document, @Authorized for any authenticated reader, and @RolesAllowed (as above) for named
roles. Only the public policy omits securityScheme; the others require it:
@RestApplication( name = "internal", path = "/api/internal", resources = {InternalResource.class})@ApiDocs(policy = InternalApi.DocsPolicy.class, securityScheme = "bearerAuth")public interface InternalApi {
@Authorized public interface DocsPolicy extends AccessPolicy {}}bearerAuth is the scheme of the JWT handler that JwtAuthModule registers (see
dev.vertique:vertique-rest-auth-jwt). The example binds its JWTAuth from the configured
jwt.hs256Key and keeps no key in its sources. It sets neither jwt.validation.issuer nor
jwt.validation.audience; both are unset by default, and the bearer handler then accepts any
unexpired token signed with the key, whatever its issuer or audience. Set both in production (see
dev.vertique:vertique-rest-auth-jwt).
3. Add document metadata in configuration. The management interface declares no
@OpenAPIDefinition, so its info comes from configuration; the public document gets a server URL.
Configuration never holds the access policy:
{ "apidocs": { "documents": { "public": { "serverUrl": "https://api.example.com/api/public" }, "management": { "info": { "title": "Management API", "version": "1.0" } } } }}4. Read the documents. /apidocs/public/openapi.json and .yaml answer any caller.
/apidocs/management/openapi.json and .yaml answer 401 without a token, 403 to a token
without the admin role, and 200 with Cache-Control: private, no-store to an admin. The
example also serves a Redoc page at /apidocs/ui/ (see
Serving a UI under the prefix).
Protected Documents
Section titled “Protected Documents”A document whose declaring interface carries @ApiDocs(policy = <policy>, securityScheme = "<scheme>"), with a policy that is not exactly one @PermitAll, is served only to callers the
security chain admits.
@ApiDocs(policy = ManagementApi.DocsPolicy.class, securityScheme = "bearerAuth")@RestApplication(name = "management", path = "/api/management", resources = {AdminResource.class})public interface ManagementApi {
@RolesAllowed("docs-reader") public interface DocsPolicy extends AccessPolicy {}}Access comes from the annotation only
Section titled “Access comes from the annotation only”- Declared in code.
securitySchemenames the scheme that authenticates a reader, andpolicysays who of those readers may read. The policy declares its requirements with@RolesAllowed,@Authorized(authentication only, or with scopes),@RequiresAction, a combination of them, or@DenyAll. A policy that is exactly one@Authorizedwithout scopes admits any caller the scheme authenticates, and an INFO line says so at startup (see Startup log lines). - Classification. A policy that is exactly one
@PermitAllis public; every other valid policy is protected,@DenyAllincluded. A protected document never falls through to a later mount and is never cached by a shared cache. - No configuration override.
apidocs.documents.<name>accepts onlyenabled,info, andserverUrl, and none of them changes who may read the document. - Required action. A policy with
@RequiresActionmakes the action gate of the equally annotated resource method run for the document: the action must be registered with the application’sActionRegistry, anAuthorizermust be bound, and the document application’s registrations own the permits. The framework grants nothing for it.
One synthetic operation per form
Section titled “One synthetic operation per form”Each form, <apidocs.path>/<name>/openapi.json and <apidocs.path>/<name>/openapi.yaml, is a
framework synthetic operation, apidocs:<name>:json or apidocs:<name>:yaml, that answers GET
and HEAD. Its descriptor reports the documented application’s name from applicationName() and
the annotations of the equally annotated resource method: @SecurityRequirement(name = "<scheme>")
plus the direct requirements of the document’s policy.
The route runs, in order:
- the completion recorder, which records the synthetic operation as the request’s operation (see Events and metrics);
- the authentication handler of the scheme;
- every registered
OperationHandlerContributor, application contributors included, in the order resource routes use (phase, then priority), each given the operation’s effective policy and the action the policy requires, if any; - the document handler.
The document handler evaluates the entity tag and If-None-Match only after the whole chain has
passed, so a denied caller never learns the entity tag or gets a 304.
Supported composition
Section titled “Supported composition”A protected document is enforced only in a composition that includes the framework’s real
authentication and security modules (AuthModule and SecurityModule, or a module that includes
them, such as JwtAuthModule), beside the handler of the document’s scheme. A composition that binds
only the authentication-enforcement marker enforces nothing for a policy that needs identity, role,
scope, or action checks; it is unsupported, and a protected document in it is not a sound deployment
even where startup accepts it. Where a policy needs an action, the ActionRegistry and an
Authorizer of the application belong to the composition too.
Answers
Section titled “Answers”| Situation | Response |
|---|---|
| The chain admits the caller and the document is stored | 200 or 304 as for a public document (see Responses), with Cache-Control: private, no-store and the Vary of Caching |
| The scheme does not authenticate the caller | 401 problem response |
| A contributor denies the authenticated caller, or the policy denies every reader | 403 problem response, or the status the contributor fails the request with; a policy that denies every reader authenticates first, so an anonymous reader gets 401 |
The policy’s AuthorizationDecisionPoint or Authorizer could not decide: its future failed, its deadline elapsed, or the resilience runtime had closed |
503 problem response, distinct from a 403 denial; the document is never served or revalidated |
| The chain admits the caller on a trailing-slash variant of the URL | 404 problem response |
| The chain admits the caller and the document is not stored | 503 problem response, logged at ERROR |
A failure without an explicit 4xx or 5xx status |
500 problem response, logged at ERROR |
A problem response is application/problem+json
{"type":"about:blank","title":"<reason phrase>","status":<code>} with Cache-Control: no-store
and no ETag. It is written by the document route’s own failure handler, which never continues:
the application’s error pipeline, later mounts, their failure handlers, and error interceptors never
see the request.
Name disclosure
Section titled “Name disclosure”Authentication runs before any other check, so an unauthenticated request for an enabled
protected document’s name is answered 401, which discloses that the name exists. A name without an
enabled document has no route and falls through to the later mounts. Methods other than GET and
HEAD on a protected document URL are not answered by the document route: they continue to later
mounts without the security chain and are answered as those mounts answer them. When no later mount
claims the request, the router answers 405, for example
POST /apidocs/management/openapi.json, where a request for an unknown name ends with 404. No
document content is reachable that way.
Events and metrics
Section titled “Events and metrics”- Security events. A document request emits the authorization-decision and
credential-rejection events (
AuthorizationDecisionEvent,CredentialRejectedEventfromvertique-security-core) an equally annotated resource request emits. - Completion. Each protected read or denial completes as a
RestRequestCompletedEventwhoseoperation()is the synthetic descriptor, the same instance the contributors received, carrying the documented application’s name. The REST request metrics count it under that operation. - Public reads. A public document read is claimed by no operation and completes as an
HttpRequestCompletedEvent.
Fail-closed startup
Section titled “Fail-closed startup”A protected document whose chain cannot be built fails startup while the docs mount creates its router. The documentation router is left with no route, and the server never listens.
- Unknown scheme or no enforcement.
securitySchemenames no registeredSecuritySchemeHandler, or authentication enforcement is not installed for a policy that is not public. A policy that denies everyone needs both too. Checked before any route is registered; each violation starts withApplication '<name>' (declared by <interface>):, and several violations are listed one per line underInvalid @ApiDocs values:. - No authentication handler. The scheme’s handler registered no authentication handler, or a
contributor rejects the operation while its route is built, for example an action the policy
requires that no registry holds. The message starts with
Protected API document of application '<name>' (access policy: @ApiDocs on <interface>), and every document route already installed is removed again.
Served Contracts
Section titled “Served Contracts”An application whose contract location is its own serves that contract as its document. The
document still needs @ApiDocs, is served at the same two URLs with the same access and caching,
and is stored once per component like a generated document, but nothing in it is assembled from the
running code.
When the contract is the document
Section titled “When the contract is the document”The application’s effective contract location, as vertique-rest-jaxrs resolves it, decides:
jaxrs.applications.<name>.openapiPath, when configured;- otherwise the declaring interface’s non-empty
@RestApplication.openapiPath; - otherwise the shared
jaxrs.openapiPath.
Under 1 or 2 the document is the contract at that location. Under 3 the document is generated: the
shared jaxrs.openapiPath is never served as a document (see Startup Checks for
the shared contract under openapi-contract).
@ApiDocs(policy = PartnerApi.DocsPolicy.class)@RestApplication(name = "partner", path = "/api/partner", openapiPath = "openapi/partner.yaml", resources = {PartnerResource.class})public interface PartnerApi {
@PermitAll public interface DocsPolicy extends AccessPolicy {}}The contract supplies its own info, so the document needs no apidocs.documents.<name> entry. An
entry may still set enabled; a configured info or serverUrl is refused (see
Startup checks of a served contract).
Served as parsed
Section titled “Served as parsed”- Unchanged. Both forms are written from the one parsed tree, members in parsed order. Nothing
is added, removed, or rewritten: no enrichment, no redaction, no
x-vertique-validationmember, andopenapi,info, andserversexactly as written. - JSON and YAML. The JSON form is compact UTF-8; the YAML form is written with Jackson’s default YAML settings. Parsing either form gives a tree equal to the parsed contract. The file’s own bytes are not served, so its formatting and YAML comments are not kept.
- Entity tags. Each form carries a strong entity tag computed as for a generated document (see Responses).
- Format by extension. The text after the last
.of the contract location, compared ignoring letter case, chooses the parser:jsonfor JSON,yamlorymlfor YAML. Any other extension, or none, is refused before the location is read. A JSON contract with content after its one JSON value is refused as not valid JSON. A YAML contract holds exactly one document: a second document is refused as not valid YAML. A YAML contract larger than the YAML parser’s code-point limit (about 3 MB, the limit the request-validation library applies too) also fails as not valid YAML. - No YAML anchors. YAML anchors, aliases, and
<<merge keys are not expanded: an alias is served as its anchor’s name, while the validation strategy expands them. Write a served contract without anchors, aliases, and merge keys.
Where the contract is read from
Section titled “Where the contract is read from”The location is read through the Vert.x file system, as the openapi-contract request-validation
strategy reads it, so both read the same file:
- Absolute path. That file.
- Relative path. The working-directory file of that name when one exists, otherwise the classpath resource of that name.
- Shadowing. When a relative path names a working-directory file while a classpath resource of
the same name also exists, the file shadows the resource: it is served, and under
openapi-contractit is the contract requests are validated against. A WARN says so (see Warnings of a served contract). Keep the working directory controlled: a file placed there replaces the packaged contract. - Resolved location. The startup INFO line names where the contract resolved: a file as its absolute, normalized path (symbolic links not resolved), a classpath resource as its class-loader URL (see Startup log lines).
Loaded once
Section titled “Loaded once”- Once per component. The contract is read, parsed, and checked when the application’s mount publishes, on a worker thread, once per component however many server instances it deploys. Every other instance compares its own mount as for a generated document (see Several server instances).
- Frozen. The stored bytes never change: an edit to the file after the load is not served.
- Retry after failure. A failed load fails every instance waiting on it and is discarded, so a later deployment in the same component loads the contract again.
- Nothing fetched. No reference is followed outside the document, and no URL in it is resolved.
- No time bound. The read of a contract has no time bound. A contract path that never returns, such as a named pipe with no writer, stalls startup without a log line, and every other instance waits with it. Name a regular file or a classpath resource; the location is not checked for this.
Startup checks of a served contract
Section titled “Startup checks of a served contract”Every check below stops startup. The checks run in three stages.
Document selection. While the enabled documents are selected, metadata that would rewrite the
contract fails with a ConfigurationException, checked in this order, the first one found failing:
a configured apidocs.documents.<name>.info, a configured apidocs.documents.<name>.serverUrl, and
an @OpenAPIDefinition on the declaring interface itself. A configured value counts whatever it
holds, and none is echoed:
Application '<name>' (declared by <interface>) serves its own OpenAPI contract as its document, butconfigures 'apidocs.documents.<name>.info', which would rewrite that contract; the served documentis never rewritten, so remove itComposition. Before any router is created, the composition validator reports, in the
HttpVerticle’s Invalid mount configuration: list:
- One location for two documents. Two or more enabled documents whose applications serve their
own contracts from one location:
apidocs.documents: applications '<a>', '<b>' serve documents with one contract location; each application serving its own contract as its document needs a contract location of its own. Locations are compared after path normalization, so./a.yamlanda.yamlare one location, while an absolute path and a relative path naming the same file stay distinct. Letter-case variants of one file on a case-insensitive file system, and symbolic links to one file, also stay distinct; the operation binding still refuses a contract that describes another mount’s routes. - Unparsable location. A location that cannot be parsed as a path:
apidocs.documents.<name>: application '<name>' (declared by <interface>) serves its own contract as its document, but its contract location (<setting>) cannot be parsed as a path.
Neither names the location. <setting> is @RestApplication.openapiPath on <interface> or
jaxrs.applications.<name>.openapiPath.
Load. When the mount publishes, a RestConfigurationException starting
apidocs.documents.<name>: the contract of application '<name>' (<setting>) and ending:
at '<location>' has an unsupported extension; a contract location ends with .json, .yaml, or .yml;cannot be read;is not valid JSONoris not valid YAML.
No cause is attached: neither the message nor a cause carries the file system’s or the parser’s text, or any byte of the file.
Content. The parsed contract is then checked against the mount’s routed operations. A Link Object
reached through a local $ref from a links object is checked like an inline one, at the
resolved Link’s own location. Every
violation found is reported in one RestConfigurationException, sorted and joined by ; :
The served contract of application '<name>' is refused: <violations>.
| Refused | Violation |
|---|---|
The root is not an object, or openapi is not a string 3.0.<n> or 3.1.<n>; OpenAPI 3.2 is not accepted, nor a version without its patch number. A file refused here reports that one violation only and is checked no further |
the document root must be OpenAPI 3.0 or 3.1, and it is not an object, or member /openapi must be OpenAPI 3.0 or 3.1, a string of the form 3.0.<n> or 3.1.<n> |
A string $ref anywhere that does not start with #/ |
member <pointer> is not a local reference: a reference must start with #/ |
A member named operationRef or $id anywhere |
member <pointer> is not allowed in a served contract |
A local reference the checks follow (Path Items, Callback Objects, parameters, request bodies, form schemas, Link Objects in a links object) that does not resolve, or that closes a cycle |
the reference at <pointer> does not resolve within the document, or ... closes a reference cycle |
An Operation Object without a non-blank string operationId |
operation object <pointer> has no operationId |
Two Operation Objects with one operationId |
operationId '<id>' is repeated at <pointers> |
An operationId the mount does not route, anywhere it must name a routed operation (see Operation ids) |
operationId '<id>' is not routed by the mount (at <pointers>) |
| A route operation whose HTTP method or path differs from its routed operation | operation object <pointer> with operationId '<id>' is not bound to its routed operation: its HTTP method or path differs |
| A routed operation that is not hidden and has no route operation | routed operation '<id>' is not described |
| A webhook or callback operation that reuses a routed operation id | webhook or callback operation <pointer> reuses a routed operation id '<id>' |
| A parameter, form property, or Link member that describes a hidden input (see Hidden inputs of a served contract) | parameter <pointer> of operation '<id>' describes a hidden input, and the like |
| A form request-body schema that is not a plain object schema (see Form request bodies) | form request-body schema <pointer> of operation '<id>' is not a plain object schema |
- What a message carries. Operation ids, JSON Pointers (without a leading
#), and fixed wording, each control character rendered as a Java-style Unicode escape. Never a description, example, schema, or reference value of the contract. - What names the location. Only the unsupported-extension refusal, the shadowing warning, and the source INFO line name the contract location or where it resolved, each rendered with every control character escaped.
- No location. An application that serves its own contract but has no contract location fails
provisioning with a
ConfigurationExceptionnaming the application, its declaring interface, andhas no contract location.
Operation ids
Section titled “Operation ids”Path Items are found under paths, under webhooks, in Callback Objects (components.callbacks
and the callbacks of every operation found), and under components.pathItems, following local
references; x- keys under paths and in a Callback Object are skipped. Each Operation Object
(get, put, post, delete, options, head, patch, trace) is counted once, at its own
location. Ids are compared exactly.
Where the operationId is |
Checks |
|---|---|
A route operation: its Path Item is under paths, or reached from there by reference |
Must be routed by the mount; bound to that routed operation; checked for hidden inputs and form schemas; describes that routed operation |
A webhook or callback operation: its Path Item is otherwise under webhooks or in a Callback Object, or reached from there |
Must not be a routed id; otherwise not checked against the mount: never bound, and exempt from the unrouted check |
Any other Operation Object: an unreferenced components.pathItems entry |
Must be routed; checked for hidden inputs and form schemas; describes nothing |
Any other operationId member, such as one in a Link Object or inside an example |
Must be routed; in a Link Object, the Link is checked for hidden inputs |
- Binding. A route operation’s method key must be its routed operation’s HTTP method (ASCII
case ignored). Every
pathskey that reaches its Path Item, with each{...}variable reduced to{}, must equal the routed operation’s rendered path, either mount-relative (/items/{id}) or prefixed by the mount path (/api/partner/items/{id}); for the path/, the mount path alone is accepted too. Variable names need not match. - Completeness. Every routed operation that is not hidden needs a route operation.
- Hidden operations. Routed ids include hidden operations, so a hidden operation may be described or left out.
Hidden inputs of a served contract
Section titled “Hidden inputs of a served contract”A route operation, and any other Operation Object naming a routed operation, is checked against the hidden inputs of that routed operation (see Hidden inputs). The checks read only the positions listed here; the positions under Not checked are not read.
- Parameters. Every Parameter Object of the operation and of the Path Items that hold or
reference it, local references followed, is refused when its
in(ASCII case ignored) andnamematch a hidden input:path,query, andcookienames exactly,headernames ignoring ASCII case, as the runtime binds them. - Form properties. A property named like a hidden form input, in any schema of the form schema’s reference chain.
- Links. Each key of a Link Object’s
parametersis read with an optionalpath.,query.,header., orcookie.qualifier and refused when it names a hidden input at that location; the whole key is also read as an unqualified name, matched at every parameter location. A Link’srequestBodyis refused when the operation it names has a hidden form input.
Not checked
Section titled “Not checked”A served contract is written by the developer, and it can still name a hidden input in the positions below. Such a contract starts, and it is served unchanged:
- Dynamic references and anchors. A schema reached through
$dynamicRefor$recursiveRef, or named by$anchor,$dynamicAnchor, or$recursiveAnchor. The checks follow none of them, so a schema they name is not checked for hidden form fields. - Form names outside
properties. A hidden form input named as a key of a media type’sencoding, inrequired, or independentRequired. - Runtime expressions. A runtime expression such as
$request.header.X-Debugin a Link parameter value or in a callback key. x-members underpaths. A member shaped like a Path Item under anx-key ofpaths: only itsoperationIdis checked.- Referenced maps. A
linksmap given by$ref, or a Linkparametersmap given by$ref. A single Link given by$refinside alinksobject is checked. - A hidden request body. An operation whose entity parameter carries
@Parameter(hidden = true)may still have itsrequestBodydescribed, and a LinkrequestBodynaming that operation is not refused. - Unreferenced parameters. A
components.parametersentry that names a hidden input and that no operation references.
A generated document leaves hidden inputs out in every position. Keep hidden inputs out of these positions of a served contract.
Form request bodies
Section titled “Form request bodies”For each application/x-www-form-urlencoded or multipart/form-data media type of a checked
operation’s request body (compared without parameters, ignoring ASCII case), the schema, local
references followed, must be a plain object schema, whether or not the operation hides an input:
- no schema of the reference chain carries
allOf,anyOf,oneOf,not,if,then,else,dependentSchemas,patternProperties,propertyNames,additionalProperties, orunevaluatedProperties; - the last schema of the chain has a
propertiesobject, and no schema has apropertiesmember that is not an object.
Other keywords, such as dependencies, $dynamicRef, or $recursiveRef, are not refused, and the
runtime may not enforce them (see Contract and runtime). A schema named by
$dynamicRef or $recursiveRef is not checked for hidden form fields (see
Not checked); do not use them in a form request body.
externalValue and other URLs
Section titled “externalValue and other URLs”An example’s externalValue, externalDocs.url, the servers URLs, and every other URL of the
contract are served as written. An externalValue is a URL a client of the document may fetch; the
module never fetches it, nor any other URL or reference. Keep credentials and internal hosts out of
the contract.
Warnings of a served contract
Section titled “Warnings of a served contract”All are logged on logger dev.vertique.rest.openapi.docs.DocumentWarnings, once per document and
component, and only once the contract is loaded and checked; a failed load logs none. They follow
the source INFO line, in this order:
- Shadowing.
apidocs.documents.<name>: the contract location '<relative path>' of application '<name>' is relative, and the working-directory file of that name shadows the classpath resource of the same name; the working-directory file is served and validated against. servers. Whenservers[0].urlis not a string equal to the mount path without its trailing/*(/for the root mount), noserversand an empty array included:apidocs.documents.<name>: the contract of application '<name>' does not list the mount path '<mount path>' as the url of its first server; the served document is not rewritten. The contract is served unchanged.- Operations that restrict callers. For a
PUBLICdocument, the warning of Operations that restrict callers, computed from the mount’s operations that are not hidden, each path rendered as a generated document renders it. It never reads the contract’ssecurity, so it names exactly what the generated document of that mount would name.
Each message is one line; it is wrapped here for reading.
Access and caching
Section titled “Access and caching”As for a generated document: a PUBLIC document is served to any caller, and a PROTECTED one
through the same security chain (see Protected Documents). Cache-Control,
Vary, entity tags, and 304 answers follow Caching, including its warning that
targeted cache headers such as CDN-Cache-Control are not removed from a protected document
response. The contract’s own security and securitySchemes change neither who may read the
document nor how it is cached.
Contract and runtime
Section titled “Contract and runtime”- Under
web-validationornone. The contract plays no part in request validation. Nothing checks that the contract’s schemas match what the runtime enforces; the startup checks cover operation ids, methods, paths, hidden inputs, and the shape of form schemas only. Keep the contract in step with the code. - Under
openapi-contract. The strategy validates requests against the same file the document serves. An application on the shared globaljaxrs.openapiPathwith an enabled document is still refused, with the alternative of serving that file behind an access check (see Startup Checks); an application with its own contract is served. serversunderopenapi-contract. The strategy loads the served contract withvertx-openapi, which accepts relative and absolute server URLs but fails startup on one it cannot parse. Theserverswarning expectsservers[0].urlto equal the mount path, a relative URL, which the strategy loads without complaint.
Document Content
Section titled “Document Content”A document is built from the operations its application’s JAX-RS mount publishes, from the schemas the mount captured for their inputs, and from the output schemas the module generates for their responses (see Operation Responses). Nothing is read from a request. This section and the ones that follow describe a generated document; a served contract is served as parsed (see Served Contracts).
Root members
Section titled “Root members”The root members are written in this order:
openapi:3.1.1.info: seeinfo.jsonSchemaDialect:https://json-schema.org/draft/2020-12/schema.servers: exactly one entry. Itsurlis the configuredapidocs.documents.<name>.serverUrlwhen present; otherwise the mount path without its trailing/*, and/for the root mount. It is never taken from request headers such asHostorX-Forwarded-*.paths: one entry per rendered path, keys in natural order.components:schemas, thensecuritySchemes(see Operation Security), each with its keys in natural order and each left out when empty;componentsitself is left out when both are.tags: one Tag Object per tag a published operation declares with@Tag, sorted by name; left out when there is none (see Tags).x-vertique-validation:{"patternDialect": "java.util.regex"}in a public document (see Patterns and Validation disclosure).
Paths and operations
Section titled “Paths and operations”- Path keys. Each key is the operation’s JAX-RS path template relative to the mount, with every
variable written as
{name}:{id: [0-9]+}becomes{id}. The regular expression of a variable never reaches the document. - Equivalent paths. Two templates whose rendered keys have the same literal text with variables at the same positions are equivalent. They share one path item only when they use the same variable names and differ in method; otherwise startup fails (see Startup failures).
- Methods. Within a path item each operation is keyed by its lowercase HTTP method, in the order
get,put,post,delete,options,head,patch,trace. - Operation Object.
tags,summary,description, andexternalDocs, each only when set (see Swagger annotations); thenoperationId(the runtime operation id),parameters(left out when empty),requestBody(left out when the operation has none),responses(see Operation Responses),deprecated(onlytrue), and lastsecurity, only when the operation declares a security requirement (see Operation Security). - Hidden operations. A hidden operation is not listed (see Hidden operations).
Parameters
Section titled “Parameters”Every visible input that is neither the request body nor a form input becomes a Parameter Object. Method parameters come first, then the fields of composite beans, each group in binding order. A hidden input becomes nothing (see Hidden inputs).
The members are written in this order: name, in, description, required, deprecated,
schema, example, examples, each optional member only when set.
nameandin. The bound name and the lowercase location (path,query,header,cookie).description. Thedescriptionof the input’s first@Parameterannotation; when that is blank or absent, thedescriptionof that annotation’s ownschema. Left out when neither is set.required. Written astrueonly when the input is certainly required, such as a path parameter. It is never written asfalse: an input whose requiredness is not certain, such as a primitive query parameter without@DefaultValue, carries norequiredmember.@Parameter(required = true)never adds it (see Agreement with the runtime).deprecated,example,examples. From the input’s first@Parameter(see Swagger annotations).schema. The schema captured for the input, published unchanged. It is published inline, unless it holds a$refor$defsat a schema position; then it becomes a component and the parameter references it (see Components). A captured parameter schema may not hold thepropertyNameskeyword (see Refused constructs).
Unenforced inputs
Section titled “Unenforced inputs”An input with no captured schema, and every composite-bean field, is unenforced. Its schema is
{"default": "<raw @DefaultValue text>"} when it declares a @DefaultValue, and the empty schema
{} otherwise. Nothing is derived from its Java type or its constraint annotations: a @BeanParam
field int page with @DefaultValue("1") publishes {"default": "1"}, a string default and no
type.
Hidden operations
Section titled “Hidden operations”An operation is hidden when either holds:
@Hidden(io.swagger.v3.oas.annotations.Hidden) is present on the resource method, on the same method of a superclass or of an interface the resource implements (a JAX-RS interface included), on the resource class, on a superclass, or on an implemented interface; or- an
@Operationwithhidden = trueis present on the resource method or on the same method of a superclass or implemented interface. Any of those@Operations counts, not only the first one, which supplies the operation’s other members.
A composed annotation also hides, one meta level deep:
- an annotation on the resource method whose own type carries
@Hiddenor@Operation(hidden = true)hides the operation; and - an annotation on the resource class whose own type carries
@Hiddenhides the operation.
Nesting deeper than one level does not count: an annotation whose type is itself only annotated with
a further annotation carrying @Hidden does not hide an operation.
Hiding applies to the document this module serves only. A build-time specification generator may
still list an operation hidden through a composed @Hidden (swagger-core’s reader, for example,
does not resolve it).
A hidden operation is removed from the document before its paths are rendered and before any of its content is checked:
- Nothing of it is published. Its Operation Object, its path item when no visible operation is left there, every component keyed by its operation id, and every root tag that only it declared are absent. It references no security scheme, so a scheme that only hidden operations require is neither published nor asked for its description.
- Nothing of it is checked as content. Its paths, inputs, schemas, redaction manifest, hidden members, annotations, tags, examples, return type, and declared responses are neither verified nor checked, no output schema is generated for it, and it causes no warning.
- It changes no disclosure. The
reservedNamesRefusedandhiddenInputsmembers of Validation disclosure are computed over the visible operations only. - Its route still answers. Hiding changes the document only: the route, its validation, and its security are unchanged.
- Route-level checks still see it. A hidden operation still fails startup when it uses a reserved operation id or its route can answer a document URL, and it is part of the comparison between server instances.
Hidden inputs
Section titled “Hidden inputs”An input is hidden when the operation inventory of dev.vertique:vertique-rest-jaxrs flags it
hidden. The document reads only that flag and never re-derives hiding from annotations. The
inventory flags an input hidden when:
- the input itself carries
@Parameter(hidden = true)or@Schema(hidden = true), or, for a composite-bean field or component,@Hidden; - a hidden method-level entry names it: a
@Parameter(hidden = true)on the method, an entry of@Parameters, or an entry of@Operation(parameters = ...), matched by name (exactly, except that a header name matches ignoring ASCII letter case) and by location (an entry without a location matches every location); - it is a field of a
@BeanParamor@RequestParamsparameter that carries@Parameter(hidden = true)or@Schema(hidden = true), or whose type carries@Hidden: hiding the composite hides all its fields.
A hidden input appears nowhere in the document: in no parameter list, form body, request body, component, or example.
- Hidden body. The operation publishes no
requestBody. - Hidden form fields. Hidden form fields, named file parts included, leave the form body and do not choose its default media type. A form whose fields are all hidden publishes no request body.
- Removed before every other check. Apart from the path-parameter check below, hidden inputs are left out before any check of the document runs, so a hidden input is never verified, redacted, or checked, never collides with a visible one of the same name and location, and its Swagger annotations are neither checked nor warned about.
- Binding is unchanged. The request still binds and validates every hidden input exactly as before; hiding changes the document only.
- Path parameters cannot be hidden. A document must describe every variable of a path template,
so an operation that hides a path parameter fails startup:
<subject>: operation '<id>' hides its path parameter '<name>'; a document must describe every path variable, so a path parameter cannot be hidden, where<subject>isApplication '<name>' (declared by <binary name>) at mount '<mount path>'.
Request bodies
Section titled “Request bodies”The Request Body Object holds description (when set), content, and required (when written), in
that order, and nothing else: no example, examples, or deprecated. Each Media Type Object holds
schema, then examples or example when the request body’s @RequestBody supplies one (see
Swagger annotations).
- Body input. The operation’s visible body input becomes
requestBody, with one media type per type in@Consumes, orapplication/jsonwhen the operation declares none. Each media type references the one component<operationId>.request, which holds the captured body schema after reserved-name redaction. When no body schema was captured, each media type holds the empty schema{}. required. The request body carriesrequired: trueexactly when a request-validation gate is installed for the operation and the captured body schema, evaluated as the gate evaluates it, rejects an absent body. Otherwise it carries norequiredmember.- Form inputs. When the operation has no body input, its visible form inputs and named file
parts become the request body instead: an object schema with one property per input, in binding
order. The media types are those in
@Consumes; when the operation declares none,multipart/form-datafor an operation with a visible named file part andapplication/x-www-form-urlencodedotherwise. A named file part’s property is{}; any other property is its captured schema (inline or a component, as for a parameter) or the unenforced schema above. A form request body never carriesrequired, takes only itsdescriptionfrom@RequestBody, and carries no examples. - Body and form inputs together. When an operation binds a body, its form inputs are not published.
Components
Section titled “Components”Component keys are built from the runtime operation id:
| Input or output | Component key |
|---|---|
| Request body | <operationId>.request |
| Parameter | <operationId>.<location>.<name>, location in lowercase |
| Form field | <operationId>.form.<name> |
| Inferred response body | <operationId>.response |
| Declared response content | <operationId>.response.<status>, or <operationId>.response.<status>.<n> (see Response components) |
| Declared response header | <operationId>.response.<status>.header.<name> |
| Relocated definition | <component>.<def> |
Every character outside [A-Za-z0-9._-] in a key is replaced by _. Two components with the same
key fail startup. Response components are published after the operation’s input components.
Reserved-name redaction
Section titled “Reserved-name redaction”The request-body schema the framework’s input generator builds, which is the schema the
request-validation gate validates, can refuse names the body type binds but does not publish, such
as a @JsonIgnore or @Schema(hidden = true) member beside extra keys a @JsonAnySetter accepts.
It does so with an assertion under propertyNames that spells each name out: an enum of the names
for a case-sensitively bound type, or a pattern of their ASCII case folds for a case-insensitively
bound one. The generator records the location of every such assertion, copies included, in a
redaction manifest bound to the schema’s content. dev.vertique:vertique-json-schema describes when
the generator emits these assertions.
The document publishes the body schema minus exactly those recorded assertions, so the names never appear in it while the gate still refuses them:
- Exactly the recorded locations. Each recorded assertion is removed from the document’s own
copy, before relocation. Nothing is matched by shape and
nothing is simplified: an
allOfleft with one element stays anallOf. A body that reserves no name is published unchanged. - What stays. The refusal of non-ASCII keys of a case-insensitively bound type, and a
propertyNamesthat a JSON mapper profile’s own schema override declares, are not recorded and stay in the document. - The gate is unchanged. The captured schema the gate validates with is never changed, so every request is accepted or refused exactly as before.
Fail-closed. A captured body schema is published only when it carries the framework’s redaction
manifest and its content matches that manifest. Only the framework’s input generator binds one, so
a custom OperationSchemaSource, or one that decorates the framework’s source, fails startup when
it returns a body schema it built, replaced, or edited, or one without the manifest:
<subject>: the request body of operation '<id>' carries no redaction manifest matching its content(schema source <binary name>); only the framework's schema generator binds one, so the source mustreturn the generated body schema and its manifest unchanged<binary name> is the runtime class of the bound source. A manifest whose recorded locations do not
all resolve in the body schema fails startup too, before anything is removed:
<subject>: the request body of operation '<id>' carries a redaction manifest that does not resolvein its schema (schema source <binary name>)Each message is one line; it is wrapped here for reading. Neither message echoes a location, a name, or schema text. A hidden body is never verified. These checks run only for an application with an enabled document: without one, a source that binds no manifest starts and routes unchanged (see Nothing is built without an enabled document).
Decimal numbers. Every captured schema is copied into the document through a default JSON
mapper, which reads a decimal number as a double. A custom OperationSchemaSource that puts a
BigDecimal into a schema, such as a multipleOf or minimum with more precision than a double
holds, therefore publishes it as a double. The framework’s source already holds such values as
doubles, so its schemas publish unchanged.
Relocation of local definitions
Section titled “Relocation of local definitions”A schema published as a component is relocated on a copy the document owns, after reserved-name redaction for a request body; the captured schemas the request-validation gate uses are never changed.
- The root
$defsis removed, and each entry<def>becomes component<component>.<def>. - Every fragment-only
$refat a schema position, in the component and in each relocated definition, is rewritten:#becomes#/components/schemas/<component>;#/$defs/<def>and#/$defs/<def>/<rest>become#/components/schemas/<component>.<def>and that followed by/<rest>;- any other
#/<pointer>becomes#/components/schemas/<component>/<pointer>.
- Nothing else changes: no
$idis added, and a root$schema, boolean subschemas, pattern text, and member order are kept.
Refused constructs
Section titled “Refused constructs”A captured input schema or a generated output schema that the document cannot publish fails startup. It is refused when, at a schema position, it holds:
$idanywhere, the root included;$anchor,$dynamicAnchor, or$dynamicRef;$defsbelow the root;- a
$refthat is not fragment-only (does not start with#); - a fragment-only
$refthat does not resolve to a schema position inside the captured schema, such as an anchor-name fragment, a pointer to a missing member, or a pointer to the$defsobject itself.
The failure names the application, its declaring interface, the mount, the operation, the input
(for a response, the output schema of status <status>, or the output schema of header '<name>' of status <status>), and the JSON Pointer of the offending keyword. It never echoes a value, a
reference, or schema text, and names a patternProperties member by its ordinal ([key-N]) rather than its pattern.
Identifiers, anchors, and nested definitions are reported before references.
propertyNames in a parameter. Only a request body carries a redaction manifest, so a captured
parameter or form-field schema that holds the propertyNames keyword at a schema position fails
startup:
<subject>: the <location> parameter '<name>' of operation '<id>' has a schema holding the keyword'propertyNames'; parameter schemas carry no redaction manifest, so a published one may not hold itA form field is named the form field '<name>' of operation '<id>' instead. The text
propertyNames as data (inside const, enum, default, examples, or example) or as a
property name (a member of properties, patternProperties, $defs, or dependentSchemas) is
not the keyword.
Order. The whole document is checked before anything is published. Hidden operations are
removed first; then the paths of the visible operations are checked, then each operation in the
order the document lists it. Within an operation, a hidden path parameter is checked first; then
hidden inputs are left out, and the visible inputs are checked: duplicate inputs; the request body
(its manifest, its refused constructs, its hidden members, and
its redaction); the parameter schemas (propertyNames, then refused constructs); the form-field
schemas, likewise; then its Swagger annotations (see
Agreement with the runtime); and then its responses (see
Order of the response checks). Once every operation is checked, the
@Tag declarations of all operations are merged in the same order (see Tags), and then the
security schemes the operations reference are resolved (see
Operation Security). The first violation fails startup. Component key
collisions are found as components are published.
Hidden members of a request body
Section titled “Hidden members of a request body”A hidden input is left out (see Hidden inputs). A hidden member inside the
type of a published request body is different: the input generator of the operation’s JSON mapper
profile describes the body type, and when that description still holds a member or type carrying
@Hidden or @Schema(hidden = true) that the generator did not leave out, startup fails. The
check runs for every published request body, whether or not a body schema was captured. It inspects
the bound Java type of the request body. A custom or decorating schema source must return the
generated description of the bound type unchanged: @Hidden members of a different type it
describes are not refused (such a document reports inputSchemaSource: custom when protected). A
refusal reads:
<subject>: the request body of operation '<id>' describes <what>, which carries <marker>; <fix><what>ismember '<member>' of <declaring type>, ortype <declaring type>for a type.<marker>is@Hidden,@Schema(hidden = true), orboth @Hidden and @Schema(hidden = true).<fix>depends on where the marker is:
| Position of the marker | Fix |
|---|---|
@Hidden on the property’s own field or getter |
The input generator ignores @Hidden; add @Schema(hidden = true) on the property’s own field or getter |
@Schema(hidden = true) declared through an annotation bundle, a mix-in, or a creator parameter |
Declare it directly on the property’s own field or getter |
| A type carrying a marker | The generator does not hide a type; declare @Schema(hidden = true) on the field or getter of each member that references it, or hide the operation |
A member the generator cannot leave out: an enum constant, @JsonUnwrapped content, a property bound through a setter, a builder, or a static factory’s creator parameter, or its value constraints, and also a case-insensitively bound nested bean described inline or a converter-bound property |
Remove it from the published type, or hide the operation |
The messages state these fixes as:
the input generator ignores @Hidden; declare @Schema(hidden = true) on the property's own field or getter;the input generator ignores @Schema(hidden = true) where it is declared; declare it directly on the property's own field or getter, not through a bundle or mix-in;the input generator does not hide a type; declare @Schema(hidden = true) on the field or getter of each member that references it, or hide the operation;the input generator cannot leave this member out where the document describes it (an enum constant, @JsonUnwrapped content, or a property bound through a setter, a builder, a static factory's creator parameter, or its value constraints); remove it from the published type, or hide the operation. This message is used for every position in the table’s last row, including the two the parenthetical does not list.
The first reported entry fails startup. When the body type cannot be inspected, startup fails with
<subject>: the request body of operation '<id>' could not be inspected for hidden members, without
the cause. The generator’s output, the request-validation gate, and the binding are unchanged: the
member still binds and keeps its constraints.
Patterns
Section titled “Patterns”Patterns are published verbatim, as written in the captured schema. They are Java regular
expressions, which the root member x-vertique-validation.patternDialect: "java.util.regex" records;
a consumer using another regular-expression dialect may read a pattern differently.
Validation disclosure
Section titled “Validation disclosure”The root x-vertique-validation depends on the document’s classification, which comes only from
the policy of @ApiDocs (public exactly for one @PermitAll); no configuration changes it.
- Public document. The root member is exactly
{"patternDialect":"java.util.regex"}, and no input carries a marker. - Protected document. After
patternDialect, the root member adds, in this order:
| Member | Value |
|---|---|
strategy |
The id of the mount’s request-validation strategy |
inputSchemaSource |
generated when the bound OperationSchemaSource is exactly the framework’s source, runtime class dev.vertique.rest.validation.AnnotationSchemaSource; custom for any other source, a subclass or a wrapper of the framework’s source included; absent when none is bound |
enforcement |
active for web-validation, disabled for none, unknown for any other strategy |
reservedNamesRefused |
true when a reserved name was removed from a published request body; otherwise absent |
hiddenInputs |
true when an input of a published operation was left out as hidden; otherwise absent |
Under web-validation only, a protected document also marks each Parameter Object and request
body that the gate does not validate with a schema with "x-vertique-validation": {"enforcedSchema": false} as its last member. A form request body is marked when any of its fields
is not validated with a schema or is a named file part, since a file part publishes no schema.
Under any other strategy no input is marked; the root enforcement already qualifies every input.
Only readers the security chain admits receive a protected document (see Protected Documents), so only they see these members.
Swagger annotations
Section titled “Swagger annotations”The document reads the Swagger annotations (io.swagger.v3.oas.annotations) of each visible
operation and of its visible inputs. Method annotations are resolved from the resource method, then
the same method of each superclass, then of each implemented interface; class annotations from the
resource class, then each superclass, then each implemented interface. Where a member is read from
“the first” annotation of a type, it is the first in that order. A blank string member counts as
unset.
Documentation never changes a published input name, location, schema, requiredness, or media type, and never edits a captured schema. An annotation that contradicts how the runtime binds an operation fails startup (see Agreement with the runtime).
| Annotation member | Published as | Precedence and rules |
|---|---|---|
@Operation.summary, @Operation.description |
The Operation Object’s summary, description |
From the first @Operation |
@Operation.deprecated |
deprecated: true |
From the first @Operation; only true is written |
@Operation.externalDocs |
externalDocs with description (when set) and url |
From the first @Operation; written only when url is set |
@Operation.tags |
The operation’s tags |
Listed first, in declared order; see Tags |
@Tag or @Tags on the method or the class |
The operation’s tags and the root tags |
See Tags |
@Operation.hidden, @Hidden |
Nothing: the operation is removed | See Hidden operations |
@SecurityRequirement, @SecurityRequirements, @Operation.security |
The operation’s security |
Not read by this module: the requirement sets the runtime resolved and enforces are published; see Operation Security |
@Parameter.description |
The Parameter Object’s description |
From the input’s first @Parameter; when blank, filled from that annotation’s schema.description |
@Parameter.deprecated |
deprecated: true |
true when @Parameter.deprecated or its schema.deprecated is true |
@Parameter.example |
example |
Not written when examples is; when unset, filled from schema.example; see Examples |
@Parameter.examples |
examples |
Wins over example; see Examples |
@RequestBody.description |
The request body’s description |
From the selected @RequestBody (below); when blank, the description of the first content entry’s schema that sets one |
@RequestBody.content examples |
The Media Type Object’s examples or example |
See Examples; never on the Request Body Object, never on a form body |
@ApiResponse and @ApiResponses on the method or the class |
The operation’s responses |
See Declared responses; their content.schema.implementation is published, unlike a request body’s |
@Operation.operationId, @Parameter.name, in, required, content, schema.implementation, array.schema.implementation, @RequestBody.content.mediaType, content.schema.implementation, required |
Nothing | Checked against the runtime; see Agreement with the runtime |
ref of @Parameter, @RequestBody, or @ExampleObject |
Nothing | Fails startup: the document declares no reusable parameters, request bodies, or examples to reference |
Other members of @Schema and @ArraySchema on an input |
Nothing | Named in a warning; see Ignored schema members |
- Which
@RequestBody. A request body is documented by exactly one@RequestBody: the first that sets any member among the one on the body parameter, the first on the method, and therequestBodyof the first@Operation. Their members are never merged. A form request body has no body parameter, so only the last two are read, and only itsdescriptionis published. - Which
@Parameter. Each Parameter Object reads the first@Parameterof its input. A@Parameteron the body parameter is neither read nor checked. A form field’s@Parameteris checked but publishes nothing. - Schema documentation members. The
description,example, anddeprecatedof a parameter’s own@Parameter(schema = @Schema(...))fill the Parameter Object as the table says; itstitleandexternalDocsare not published. A request body’s own content schema fills itsdescriptionand its Media Type Objects’example; itsdeprecated,title, andexternalDocsare not published anywhere. The documentation members of an@ArraySchema’s element schema are not published. None of these logs a warning. - DTO members. A body type’s member
title,description, andexamplereach the document only as the framework’s schema generator wrote them into the captured schema (under the member’s JSON name). The document adds, renames, and removes nothing there. - Not applied and not checked. Every member the table does not list, among them
@Parameterstyle,explode,allowReserved,allowEmptyValue, andextensions;@Operationparameters,responses(declare responses with@ApiResponseon the method or the class instead),servers, andextensions;@RequestBody.extensions; the@Contentmembers other thanmediaType,examples,schema, andarray; and the extensions of@Tag,@ExternalDocumentation, and@ExampleObject. A method-level@Parameteror@Parametersentry is read only to hide an input (see Hidden inputs).
- Operation
tags. The distinct names of the first@Operation’stags, in declared order, then of the method’s@Tags, then of the class’s@Tags, each in resolved order; the first occurrence of a name is kept and blank names are ignored. Repeated@Tags arrive as a@Tagscontainer, which is unwrapped in place. - Root
tags. Every@Tagon a published operation’s method or class contributes a Tag Object withname,description, andexternalDocs(written only with a seturl), in that order. The roottagsis sorted by name (case-sensitive) and left out when empty. A name that appears only in@Operation.tagsadds no root tag. - Conflicts. The
descriptionand theexternalDocsof one tag name are merged separately across every declaration, in document order. A declaration that leaves a member unset never conflicts, and the set value is published. Two declarations that set a member to different values fail startup, naming the tag, the operation that first set it, and the first later operation whose value differs, never either value.descriptionis compared first;externalDocsis compared as the published object. An operation whose method and class@Tags differ names itself twice.
Examples
Section titled “Examples”- Values. An example value (
@Parameter.example, a schema’sexample, or@ExampleObject.value) is published as JSON when its trimmed text parses as exactly one JSON value, and otherwise as the string it is, untrimmed."42"becomes the number42,"null"the JSONnull,"{\"a\": 1}"an object, and"42 items"stays a string. - Named examples.
@ExampleObjects become anexamplesmap keyed byname, in declaration order. Each Example Object holdssummary,description, andvalueorexternalValue, in that order, each when set. exampleswins. When an input declares both named examples and an example value, onlyexamplesis published.- Request bodies. Each
contententry of the selected@RequestBodyapplies to itsmediaType, or to every media type of the body whenmediaTypeis empty. For each media type of the body, the first applying entry, in declaration order, that declares named examples or whose schema setsexamplesupplies that Media Type Object’sexamples, or else itsexample. An entry never adds a media type. A form request body publishes no examples. - Malformed examples fail startup. An
@ExampleObjectwith a blankname, anamean earlier entry of the same array already uses, bothvalueandexternalValue, or areffails startup naming the operation, the input, and the attribute.
Agreement with the runtime
Section titled “Agreement with the runtime”Annotations document what the runtime does; they cannot change it. Each of the following fails startup, after the operation’s schema checks and before its documentation is read, in this order: the operation id, the request body, the parameters in published order, then the form fields. The first failure wins.
| Attribute | Fails when |
|---|---|
@Operation.operationId |
It is set on the first @Operation and differs from the runtime operation id |
@RequestBody.ref, @Parameter.ref |
It is set (see Swagger annotations) |
@RequestBody.content.mediaType |
It is set and is not among the media types the request body publishes |
@RequestBody.content.schema.implementation |
It is set and differs from the erasure of the bound body type; on a form request body, whenever it is set |
@RequestBody.required |
It is true and the document does not mark the request body required (see Request bodies); a form request body never is |
@Parameter.content |
It has any entry: the runtime never binds a parameter’s content |
@Parameter.name |
It is set and differs from the bound name; a header name that differs only in ASCII letter case agrees |
@Parameter.in |
It is not DEFAULT and differs from the bound location; on a form field, whenever it is not DEFAULT |
@Parameter.required |
It is true and the runtime certainly accepts a missing value (below) |
@Parameter.schema.implementation |
It is set and differs from the erasure of the bound type |
@Parameter.array.schema.implementation |
It is set and differs from the erasure of the element type of a collection or array input, or the input is neither a collection nor an array. An input whose element type is not known, such as a wildcard collection or a collection field of a composite bean, is not checked |
- Name comparison. Media types and query, path, cookie, and form-field names are compared
exactly, case included. Header names are compared ignoring ASCII letter case, as HTTP does: a
@Parameter(name = "x-trace")on a header bound asX-Traceagrees, and the document publishesX-Trace. Only ASCII letters fold, so a non-ASCII look-alike such as U+212A KELVIN SIGN never matchesk. A hidden method-level entry names a header the same way, ignoring ASCII letter case (see Hidden inputs). - Types. A primitive type and its wrapper are the same type.
- Message.
<subject>: operation '<id>' declares <attribute> on <input>, which contradicts how the runtime binds it (<fact>); documentation metadata cannot change it, so remove the attribute or make it agree, one line, where<input>isquery parameter q,header parameter X-Trace,form field f,request body, and so on, and<subject>is as in Hidden inputs. No annotation value is echoed.
Requiredness. For each parameter and form field the runtime reports whether a missing value is
rejected, with one of three answers. @Parameter(required = true) is checked against it:
| The runtime says | For example | @Parameter(required = true) |
|---|---|---|
| Required: a missing value is certainly rejected | A path parameter; an input with a null-rejecting constraint such as @NotNull, with a bean validator bound and no group sequence involved |
Agrees; required: true is written, as without the annotation |
| Not required: a missing value certainly binds | An input with @DefaultValue, without a constraint annotation, or with no bean validator bound; a non-array collection with only @NotNull |
Fails startup |
| Cannot be determined | A primitive method parameter without @DefaultValue; any other case |
Not published: the Parameter Object carries no required, and one warning names the input (see Warnings) |
@Parameter(required = false) is never checked.
Ignored schema members
Section titled “Ignored schema members”The captured schema of an input stays authoritative. Every non-default member of the @Schema in
@Parameter.schema, @Parameter.array.schema, @RequestBody.content.schema, or
@RequestBody.content.array.schema, and every non-default member of an @ArraySchema other than
schema, is not published, and the operation’s ignored-members warning names it (see
Warnings). Exempt from the warning are the documentation members description,
title, example, deprecated, and externalDocs, and an implementation that is compared (see
Agreement with the runtime).
@RequestBody.content.array.schema.implementation is not compared and is warned.
@Parameter(schema = @Schema(hidden = true)) does not hide an input: hidden there is an ignored
member and is warned. Hide an input with @Parameter(hidden = true) or a @Schema(hidden = true) on
the input itself (see Hidden inputs).
Operation Responses
Section titled “Operation Responses”Every visible operation publishes a responses object. Without @ApiResponse it is inferred from
the resource method’s return type; with one, the declared responses replace the inferred one. The
document describes responses only: the runtime never checks a returned value against it, and no
error-body schema is ever inferred.
Inferred responses
Section titled “Inferred responses”An operation that declares no @ApiResponse, on its method or its class, publishes exactly one
response:
| Return type of the resource method | Published responses |
|---|---|
void, Void, or Future<Void> |
{"204": {"description": "No Content"}} |
Future<T> or a plain type T, with a JSON-compatible produces type |
"200" with description OK and, for each JSON-compatible produces type in declared order (application/json when the method declares none), a Media Type Object whose schema is a reference to the component <operationId>.response: the output schema of T under the operation’s output profile |
String or Future<String> |
"200" with description OK and one media type per declared produces type, in declared order (application/json when none), each {}: the body is raw text, not a JSON string, so no schema is published |
| Everything decided at runtime (below) | {"default": {"description": "Response determined at runtime"}}, with no content |
The rules apply in this order, the first match winning:
- No content.
void,Void, andFuture<Void>publish204. - Type variables. Every type variable of the return type is resolved (see
Type variables); then exactly one level of
io.vertx.core.Futureis unwrapped, soFuture<Response>classifies asResponseandFuture<String>asString. A rawFuturewithout a type argument is decided at runtime, and so is a subtype ofFuture(for exampleinterface MyFuture<T> extends Future<T>): onlyio.vertx.core.Futureitself is unwrapped, so a subtype publishes the runtime-determineddefaultresponse with no content. AFutureor a subtype inside the unwrappedFuture(Future<Future<T>>,Future<MyFuture<T>>) publishes thatdefaultresponse too. - Still open. A type that still holds a type variable or a wildcard is decided at runtime and is never handed to the generator.
- Handled by the runtime.
Response,CompletionStage,ReadStream,Buffer, and their subtypes,Optional, andbyte[]are decided at runtime. So is a type bound to aResponseProducerBinding: the type itself, a superclass, or an interface of it equals a boundtype(), walked as the runtime looks a producer up. A producer binding wins before theStringand JSON rows, because the runtime consults producers before any body encoder. String. AStringis raw text under every produces type,application/jsonandtext/event-streamincluded: the runtime writes aStringentity as it is.- Event streams. Any other type whose produces list holds a type starting with
text/event-streamis decided at runtime. - JSON entities. Any other type publishes
200under its JSON-compatible produces types. A media type is JSON-compatible exactly when it containsjson, compared case-sensitively as the runtime’s JSON encoder decides:application/jsonandapplication/vnd.acme+jsonare,application/JSONis not. Other produces types are left out of the content. A type with no JSON-compatible produces type, such as one producing onlytext/plain, is decided at runtime.
The whole resolved type is generated: Future<List<Item>> publishes the output schema of
List<Item>, an array.
No 4XX, 5XX, or default response is ever added to an inferred response, and no error body is
described. Declare them with @ApiResponse (see Declared responses).
Type variables
Section titled “Type variables”Before classification, every type variable of the return type is resolved against the generic
superclasses and interfaces of the resource class the operation is registered on, the nearest
binding winning. For class ItemResource extends CrudResource<Item>, an inherited T find()
classifies as Item and an inherited Future<List<T>> list() as Future<List<Item>>.
These stay open and publish default:
- a method-level type variable, as in
<T> T any(); - a type variable of a generic resource class registered itself, as
T get()onGenericResource<T>; - a wildcard, as in
List<?>.
Declared responses
Section titled “Declared responses”@ApiResponse (or its container @ApiResponses; the two forms are equivalent) is read at two
levels:
- Method level. The resource method, the methods it overrides, and the interface methods it implements, all together as one level.
- Class level. The resource class, its superclasses, and its implemented interfaces.
The declared set is the class level’s responses plus the method level’s, the method’s declaration
winning for a status both declare (as swagger-core merges them). A non-empty declared set replaces
the inferred response entirely, also for an operation that declares nothing itself when its class
does. @Operation.responses is not read.
- Valid statuses. A status is
default, a three-digit code from100to599, or an uppercase range key from1XXto5XX. Anything else, such as2xx,600, or20, fails startup. - One declaration per status and level. A level that declares one status twice fails startup, naming the level. Because the method level includes the implementation and its interface methods, an implementation method and its interface method declaring one status differently fail too; identical declarations count once.
- Order. Response keys are published numeric codes ascending, then range keys ascending, then
default, whatever the declaration order or level. - Descriptions. A declared
descriptionis published as written. A blank one publishes the status’s reason phrase (Acceptedfor202); a code without a registered phrase publishes the phrase of its class (Successfor299); a range key publishesInformational,Success,Redirection,Client error, orServer error; anddefaultpublishesResponse determined at runtime. - Response Object.
description,headers(when any),content(when published), then itsx-extensions, in that order.
A declared success status without content
Section titled “A declared success status without content”A declared success status, a code from 200 to 299 other than 204 and 205 or the range key
2XX, whose @ApiResponse declares no content receives the inferred content when the return type
is inferable (the 200 rows of Inferred responses): the inferred media types
and, for a JSON entity, a reference to the inferred component <operationId>.response. Its declared
description and headers are kept:
| Declared status | Return type | Published content |
|---|---|---|
Success status without content |
A JSON entity or a String |
The inferred content |
Success status without content |
Decided at runtime, or no content | None; startup does not fail |
Any status with content |
Any | The declared content only, even on an inferable return |
Any other status without content: 204, 205, 3XX, 404, default, and so on |
Any | None |
So @ApiResponse(responseCode = "200", description = "The catalog item") on a method returning a
plain CatalogItem publishes its description together with the CatalogItem schema.
This is a deliberate difference from swagger-core, whose useReturnTypeSchema defaults to false,
so swagger-core publishes such a status without content. A document generated at build time by a
swagger-core based tool may therefore differ from the served document at such a status.
useReturnTypeSchema = true. A status that sets it publishes the inferred content under any
status when it declares no content; with content declared on an inferable return, the declared
content wins. On a return type that is not inferable it fails startup, whether or not content is
declared.
Response content and headers
Section titled “Response content and headers”- Media types. A
@Contentwith amediaTypeapplies to that media type. One with a blankmediaTypeapplies to each of the operation’s produces types in declared order (application/jsonwhen it declares none), JSON-compatible or not. A media type declared twice in one status, also through a blankmediaType, fails startup naming@Content.mediaType. - Schema.
schema = @Schema(implementation = X)publishes{"$ref": "#/components/schemas/<component>"}(see Response components). - Arrays.
array = @ArraySchema(schema = @Schema(implementation = X))publishes{"items": {"$ref": "#/components/schemas/<component>"}, "type": "array"}, members in that order, the component describingX. When a@Contentsets both, theschemais published and the array’s set members are warned as omitted. - Neither. A
@Contentwith neither implementation publishes its media type as{}. - Media Type Object.
schema(when published), thenexamples(when declared). - Headers. A Header Object holds
description(when set),requiredanddeprecated(onlytrue), thenschema: a reference to the component<operationId>.response.<status>.header.<name>whenschema.implementationis set, and the empty schema{}otherwise, since OpenAPI 3.1 requires a header schema. A@Headerwith a blank name is left out and warned as@Header.name. One header name declared twice in one status fails startup naming@Header.name; names are compared ignoring ASCII letter case, soX-Rateandx-rateare the same header (non-ASCII letters never fold). @Header(hidden = true)is not honored. The header is published, and@Header.hiddenis named in the omitted-attribute warning.hidden = trueon a response’s@Schemais not honored either. On a content schema, an@ArraySchema’sschemaorarraySchema, or a header schema, the implementation is still generated and published in full, andhiddenis merely warned (as@Schema.hidden, or@ArraySchema.arraySchema), not refused. To keep a type out of a document, remove the declaration or hide the operation.
Documentation members. The description, title, example, deprecated (only true), and
externalDocs (only with a url) of a @Schema that sets implementation are published beside the
reference they document, never in the component: beside the content or header schema’s $ref, or
beside the items reference of an array. Their keys are in natural order, for example
{"$ref": "#/components/schemas/getThing.response.200", "description": "The thing body", "title": "Thing"}. An example is published as JSON when its trimmed text parses as exactly one JSON value,
and as a string otherwise. Unlike a request body’s, a response schema’s example stays in the
Schema Object and never becomes the Media Type Object’s example. A member that is set to
whitespace only counts as unset and is warned. A @Schema without implementation publishes
nothing, and every member it sets is warned.
Examples. @Content.examples becomes the Media Type Object’s examples under the rules of
Examples: keyed by name, in declaration order, each with summary, description, and
value or externalValue, a value that parses as JSON published as JSON. A blank or repeated
name, both value and externalValue, or a non-blank ref fails startup naming the operation, the
@ExampleObject attribute, and @Content.examples on status <status>. The extensions of an
@ExampleObject are not published and are warned.
Extensions. An @ApiResponse.extensions entry whose name starts with x- (case-sensitive)
becomes a member of the Response Object, after content, rendered as an info extension is (see
info): @Extension(name = "x-rate-limited", properties = @ExtensionProperty(name = "limit", value = "10")) becomes "x-rate-limited": {"limit": "10"}. Any other extension is not
published and is warned.
Response components
Section titled “Response components”| Schema | Component key |
|---|---|
| The inferred output type | <operationId>.response, one per operation, referenced from every media type and status that publishes the inferred content |
A declared schema.implementation or array element implementation |
<operationId>.response.<status> |
| Several different implementations in one status | <operationId>.response.<status>.1, .2, and so on, numbered from 1 in declaration order |
A header’s schema.implementation |
<operationId>.response.<status>.header.<name> |
- One implementation, one component. One implementation used by several media types of a status,
or as both a
schemaand anarrayelement, is one component and gets no number. The same type under two statuses is two components. - Sanitizing and collisions. Every key is built as in Components: each character
outside
[A-Za-z0-9._-]is replaced by_, and two components with the same key fail startup, as do the components of operationsget:reportandget_report. The failure names the colliding component asthe output schema of status <status> of operation '<id>', orthe output schema of header '<name>' of status <status> of operation '<id>'. - Relocation and refused constructs. Each component is the generated output schema, relocated
as in Relocation of local definitions (each root
$defsentry becomes<component>.<def>) and checked as in Refused constructs. - Only what is referenced. The inferred type is generated, checked, and published only when some status publishes the inferred content, so an operation whose declared responses supply all their content never generates its return type.
Omitted response attributes
Section titled “Omitted response attributes”Every attribute of a declared response that is set to something other than its default and that the sections above do not publish is left out and does not fail startup. Among them:
@ApiResponse.refand@ApiResponse.links;@Content.encodingand every other@Contentmember exceptmediaType,schema,array, andexamples;- every
@Schemamember other thanimplementationand the documentation members, such astype,format, ormaxProperties, and every member of a@Schemawithoutimplementation; - every
@ArraySchemamember other thanschema, such asminItemsorarraySchema; @Header.ref,@Header.hidden, and every other@Headermember exceptname,description,required,deprecated, andschema;- an extension whose name does not start with
x-, named@ApiResponse.extensionswithout its name, and@ExampleObject.extensions.
One warning per operation names all of them, each as <attribute> on status <status>, in published
status order; within a status the response’s own members come first, then each header’s, then
each content’s, members in name order. It is logged at WARN on the DocumentWarnings logger once
the document is written (see Metadata warnings), and quotes no attribute value:
apidocs.documents.<name>: operation '<id>' at mount '<mount path>' declares response attributes thedocument does not publish: @ApiResponse.links on status 200, @Header.hidden on status 404Output profile and generation
Section titled “Output profile and generation”- Output profile. The operation’s output profile is the JSON mapper profile the runtime
serializes its responses with, such as one selected with
@JsonProfile, looked up in theJsonMapperProfileRegistry. Each output type is described by the framework’s output generator for that profile (AnnotationJsonSchemaGenerator.forOutputProfile), so the component holds the names and members a response carries: the profile’s naming strategy applies, a read-only member appears, and a write-only member does not. - Only the framework’s output generator. Output schemas come from that generator alone. No
OperationSchemaSourceis consulted for a response, and no schema is built from a Java type by any other means. - Once per document. Output schemas are generated while a document is assembled, once per document and component, by the composition that assembles it, with one generator per profile. Another server instance that finds the document stored only compares its publication (see Several server instances). Nothing is generated per request.
Refusals of output types
Section titled “Refusals of output types”Every published output type is checked: the inferred type (when some status publishes it), each
declared content implementation, an array element implementation included, and each header
implementation. Each is generated, then inspected for renamed members, then for hidden members,
then checked for refused constructs; the first failure stops startup.
These messages start with the document’s configuration path, followed by the usual subject; below,
<prefix> stands for
apidocs.documents.<name>: Application '<name>' (declared by <binary name>) at mount '<mount path>'.
Refused constructs and component collisions of an output schema start with the subject alone, as
for inputs. No message carries schema text, an example, or a payload. Each message is one line; it is
wrapped here for reading.
Generator failure. An exception while resolving the profile, generating the schema, or
inspecting it, for example a JsonSchemaGenerationException for a member whose
@Schema(implementation = ...) redirects a type the profile overrides, fails startup:
<prefix>: operation '<id>' cannot publish the output content schema of status <status>:generating it in output direction failedA header type is named header '<name>' schema instead of content schema, and the reason is
inspecting it for renamed members failed or inspecting it for hidden members failed when that
step throws. The status is the declared status of the content or header; the inferred type is always
named as status 200, also when it is published under 201, 2XX, or a useReturnTypeSchema
status. The generator’s exception is kept as the cause.
Renamed output property. A member that @Schema(name = ...) describes under a name other than
the one the profile’s mapper serializes fails startup, because the document would describe a
property the response never carries:
<prefix>: operation '<id>' publishes <declaring type> as an output type of status <status>; its member'<member>' is serialized as '<serialized name>' but described as '<schema name>', so the documentwould describe a property the response never carries; make @Schema(name) agree with the serializedname or remove itThe first renamed member reachable from the type is named. @Schema(name) loses to a non-empty
@JsonProperty that names something other than the member itself, and the check reflects that:
@JsonProperty("wire") @Schema(name = "label") String code publishes wire and is not refused, while
a bare @JsonProperty or @JsonProperty("code") beside @Schema(name = "label") is.
Hidden output members. The output generator ignores @Hidden, and honors
@Schema(hidden = true) only on the property’s own field or getter. A member or type that the
output schema still describes while it carries either marker fails startup, because the response
would publish what the marker is meant to hide:
<prefix>: operation '<id>' publishes an output type of status <status> that describes <what>, whichcarries <marker>; <fix><what> and <marker> are as for a request body (see
Hidden members of a request body). The first entry, by
declaring type and then member with a type before its members, is named, with the one fix its
position implies:
| Position of the marker | Fix |
|---|---|
@Hidden on a property’s field, getter, or setter, without an honored @Schema(hidden = true) |
the output generator ignores @Hidden; declare @Schema(hidden = true) on the property's own field or getter |
@Schema(hidden = true) on a setter, on a creator parameter, through an annotation bundle or a mix-in, or at another position the output generator ignores |
the output generator ignores @Schema(hidden = true) where it is declared; declare it directly on the property's own field or getter, not through a bundle or mix-in |
A type carrying either marker, a class-level @Schema(hidden = true) included |
the output generator does not hide a type; declare @Schema(hidden = true) on the field or getter of each member that references it, or hide the operation |
An enum constant, or @JsonUnwrapped content |
the output generator cannot leave this member out where the document describes it (an enum constant or @JsonUnwrapped content); remove it from the published type, or hide the operation |
- The fix that works. A member carrying
@Schema(hidden = true)on its own field or getter, with or without@Hidden, is absent from the published schema, and startup succeeds. - Input-only positions do not apply. The input generator also ignores a marker on the field or getter of a property bound through a setter, a builder, or a static factory’s creator parameter, of a case-insensitively bound nested bean described inline, of a converter-bound property, and of a map member rendered through its value constraints (see Hidden members of a request body). These are ways of binding a request; a response is serialized, and only the positions in the table are refused for it.
- Nothing is dropped. The refusal never changes the generator’s output or the response: the member is still serialized at runtime. Hide the operation, or fix the marker.
- Hidden operations are never checked, and with no enabled document nothing is checked (see Nothing is built without an enabled document).
Order of the response checks
Section titled “Order of the response checks”An operation’s responses are checked after its inputs and their annotations, in this order:
- The declared statuses: each one’s validity, then repeated statuses, at the method level and then at the class level.
- Per declared status in published order:
useReturnTypeSchemaon a return type that is not inferable, then each@Content’s examples and media types in declaration order, then the header names. - The output types: the inferred type first, when some status publishes it, then per status in published order each content implementation in declaration order, then each header implementation, each through the steps of Refusals of output types.
Component collisions are found as the document is written, after every operation is checked.
Operation Security
Section titled “Operation Security”A document states which security schemes its operations require and describes those schemes. It
publishes what the runtime enforces, never more: the requirement sets come from the operation’s
@SecurityRequirement, @SecurityRequirements, or @Operation(security = ...) exactly as
dev.vertique:vertique-rest-jaxrs resolves them for routing, and each scheme’s description comes
from its SecuritySchemeHandler.
Operation security
Section titled “Operation security”- One object per requirement set. An operation that declares a requirement publishes
securitywith one Security Requirement Object per requirement set, in declaration order. Alternatives, such as two repeated@SecurityRequirements, are separate objects. - Scopes as declared. Each object is keyed by scheme name, and its value lists the
requirement’s scopes in declaration order, or
[]when it has none. Those scopes are published, so a public document discloses the scopes its operations require. - Last member.
securityis the last member of the Operation Object. - Only when declared. An operation without a requirement has no
securitymember, and the document never writes a rootsecurity.
What is never published
Section titled “What is never published”Roles from @RolesAllowed, the scopes of @Authorized, @RequiresAction actions, @PermitAll,
@DenyAll, and checks the application makes in code are not expressible in an OpenAPI document and
appear nowhere in it. They still restrict callers at runtime. A public document that lists such
operations logs one warning (see
Operations that restrict callers).
components.securitySchemes
Section titled “components.securitySchemes”- Exactly the referenced schemes. A scheme is published when a requirement of a published operation names it. Keys are sorted by scheme name.
- From the handler. Each Security Scheme Object is rendered from the
SecuritySchemeDescriptionthat the scheme’s registered handler returns fromopenApiDescription(), asked once per referenced scheme each time the document is assembled. - Unreferenced handlers stay out. A registered handler whose scheme no published operation references, a scheme only hidden operations require included, is neither published nor asked for its description.
- Document access is separate.
@ApiDocs(securityScheme)names the scheme that would protect the document routes. It is not an operation requirement and does not by itself entersecuritySchemes; it appears there only when a published operation also requires the scheme.
Members are written in the Security Scheme Object’s field order type, description, name, in,
scheme, bearerFormat, flows, openIdConnectUrl; an empty optional field is left out, and
description is written only when the description sets one.
Description (dev.vertique.rest.core.security.scheme) |
Security Scheme Object |
|---|---|
Http.of(scheme) |
{"type":"http","scheme":"<scheme>"} |
Http.bearer(format) |
{"type":"http","scheme":"bearer","bearerFormat":"<format>"}; no bearerFormat when format is null |
ApiKey.header(name), .query(name), .cookie(name) |
{"type":"apiKey","name":"<name>","in":"header"}, "query", or "cookie" |
OAuth2.of(flows) |
{"type":"oauth2","flows":{...}}: the set flows in the order implicit, password, clientCredentials, authorizationCode, each with authorizationUrl, tokenUrl, and refreshUrl when set, then scopes, always present and sorted by scope name |
OpenIdConnect.of(url) |
{"type":"openIdConnect","openIdConnectUrl":"<url>"} |
MutualTls.of() |
{"type":"mutualTLS"} |
The JWT handler of dev.vertique:vertique-rest-auth-jwt describes its scheme as
{"type":"http","scheme":"bearer","bearerFormat":"JWT"}, under whatever scheme name it is
configured with.
Published as supplied. Descriptions, OAuth2 flow URLs, scope names and scope descriptions, API key names, and the OpenID Connect URL are published exactly as the handler supplies them. A scheme’s OAuth2 flow scopes are published as the handler lists them, including scopes only hidden or unpublished operations use, so list only the scopes the document should reveal. This module performs no OAuth or OpenID Connect processing and checks no URL. Never put credentials, internal hosts, or other secrets in a description.
Fail-closed publication
Section titled “Fail-closed publication”Each referenced scheme must be publishable, or the document fails publication and so startup.
<prefix> is as in Refusals of output types, <METHOD> <path> names
the first operation, in document order, that references the scheme, <path> is its path key, and
each message is one line, wrapped here for reading:
<prefix>: operation '<id>' (<METHOD> <path>) requires security scheme '<scheme>', whoseSecuritySchemeHandler provides no OpenAPI description; return one from openApiDescription() ordisable the document- No description. The message above. Fix it by returning a description from the handler’s
openApiDescription(), or setapidocs.documents.<name>.enabled: false. - No registered handler. The same start, ending
, but no SecuritySchemeHandler is registered for it. In a deployment the route registration ofvertique-rest-jaxrsalready refuses such an operation, before any document is assembled. - More than one handler provides the scheme. The same start, ending
, which more than one SecuritySchemeHandler provides: <class names>; each security scheme must be provided by exactly one handler, with the binary class names sorted. Neither of the scheme’s handlers is asked for a description. Fix it by providing each scheme from exactly one handler: put the description on the enforcing handler. - A kind this module does not know. The same start, ending
, whose SecuritySchemeHandler describes it as <class name>, a kind of security scheme this documentation module cannot publish, naming the description’s class.
No message echoes description content or a configuration value. With no enabled document no handler is asked for a description, so none of these checks runs (see Nothing is built without an enabled document).
Example
Section titled “Example”With the JWT handler configured as bearerAuth and a handler apiKeyAuth describing
ApiKey.header("X-Api-Key"):
@Path("/orders")public class OrderResource { @GET @Path("/read") @Produces(MediaType.TEXT_PLAIN) @SecurityRequirement(name = "bearerAuth", scopes = "orders.read") public String readOrder() { return "order"; }
@GET @Path("/search") @Produces(MediaType.TEXT_PLAIN) @SecurityRequirement(name = "bearerAuth") @SecurityRequirement(name = "apiKeyAuth") public String searchOrders() { return "found"; }
@GET @Path("/admin") @Produces(MediaType.TEXT_PLAIN) @SecurityRequirement(name = "bearerAuth") @RolesAllowed("order-admin") public String adminOrders() { return "admin"; }}The document holds (wrapped here for reading; other members left out):
"paths":{"/orders/admin":{"get":{"operationId":"adminOrders","responses":{"200":{"description":"OK","content":{"text/plain":{}}}},"security":[{"bearerAuth":[]}]}},"/orders/read":{"get":{"operationId":"readOrder","responses":{"200":{"description":"OK","content":{"text/plain":{}}}},"security":[{"bearerAuth":["orders.read"]}]}},"/orders/search":{"get":{"operationId":"searchOrders","responses":{"200":{"description":"OK","content":{"text/plain":{}}}},"security":[{"bearerAuth":[]},{"apiKeyAuth":[]}]}}},"components":{"securitySchemes":{"apiKeyAuth":{"type":"apiKey","name":"X-Api-Key","in":"header"},"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}}}The role order-admin appears nowhere in the document, while adminOrders still refuses a caller
without it.
Key Classes
Section titled “Key Classes”ApiDocs
Section titled “ApiDocs”@Retention(RUNTIME) @Target(TYPE) @Documentedpublic @interface ApiDocs { Class<? extends AccessPolicy> policy(); // required, no default String securityScheme() default ""; // empty exactly when the policy is public enum Access { PUBLIC, PROTECTED } // derived classification, not an attribute}Place it on the @RestApplication declaring interface. It enables the document named by the
application and states who may read the document routes; it guards those routes only and is not API
protection. No configuration changes the annotation’s policy. It is honored only on the declaring
interface itself. vertique-rest-jaxrs and vertique-codegen-jaxrs recognize it by its fully
qualified name, dev.vertique.rest.openapi.docs.ApiDocs (also the constant ApiDocs.ANNOTATION_NAME),
and never depend on this module.
A document whose policy is exactly one @PermitAll is PUBLIC and served to any caller; every other
valid policy is PROTECTED and served through the security chain. Access is the derived
classification the document records, never an attribute of the annotation. See
Access and Protected Documents.
OpenApiDocsModule
Section titled “OpenApiDocsModule”The Dagger module an application lists. It reads the apidocs configuration, selects the enabled
documents, and contributes the publication hook, the composition validator, and the docs mount when
at least one document is enabled (see Module Dagger Bindings).
The apidocs section, including each document’s info, is parsed into records internal to the
module, and the parsed section is not a Dagger binding: applications write configuration, not
records. ApiDocs and OpenApiDocsModule are the module’s only application types; everything in
the subpackages of dev.vertique.rest.openapi.docs is internal.
Configuration
Section titled “Configuration”| Key | Type | Default | Meaning |
|---|---|---|---|
apidocs.enabled |
boolean | true |
Global switch. false disables every document, and the rest of the apidocs subtree is then neither parsed nor checked. Must be a JSON boolean |
apidocs.path |
string | /apidocs |
Prefix under which documents are served |
apidocs.documents.<name>.enabled |
boolean | absent | false disables the document of application <name>. Absent or null keeps the decision of @ApiDocs. true is accepted only for an application whose declaring interface carries @ApiDocs. A string that is blank or made only of control characters fails startup |
apidocs.documents.<name>.info.title |
string | none | Document title. Required non-blank when info is configured; otherwise info comes from @OpenAPIDefinition on the declaring interface. Any info is refused for an application that serves its own contract (see Served Contracts) |
apidocs.documents.<name>.info.version |
string | none | Document version. Required non-blank when info is configured; otherwise info comes from @OpenAPIDefinition on the declaring interface. Any info is refused for an application that serves its own contract |
apidocs.documents.<name>.info.description |
string | absent | Optional description, written to info when present. Any info is refused for an application that serves its own contract |
apidocs.documents.<name>.serverUrl |
string | absent | The document’s servers[0].url, published exactly as configured, so it must never hold credentials or other secrets. When absent, the mount path without its trailing /* (/ for the root mount). Refused for an application that serves its own contract (see Served Contracts) |
Each key under apidocs.documents is an application name. The document list is a keyed collection:
the key becomes the entry’s name.
apidocs.enabled
Section titled “apidocs.enabled”apidocs.enabled is resolved first. When it is false, nothing else under apidocs is parsed or
checked, and no startup check of this module runs. A value that is not a JSON boolean fails startup
naming apidocs.enabled.
apidocs.path
Section titled “apidocs.path”The prefix is checked only when at least one document is enabled. It must start with /, must not
be / alone, must not end with /, must contain none of *, :, {, }, ?, #, whitespace, or
//, and must have no . or .. segment. A violation fails startup with a message that names
apidocs.path and does not repeat the value.
Document entries
Section titled “Document entries”Every key under apidocs.documents is checked, whatever the entry’s enabled value, so an entry that
only disables a document is checked too and a mistyped name never leaves a document published
unnoticed. The checks run in this order, and the first violation fails startup:
- Name. The key is the application’s name and must match
[a-z0-9][a-z0-9_-]{0,63}: lowercase letters, digits,_and-, starting with a letter or digit, at most 64 characters. No application can be declared under any other name. - Declared application. An application of that name must be declared, active or not. An entry that names no declared application is refused. An entry for an inactive application is accepted and publishes nothing.
- Supported keys. The entry holds only
enabled,info, andserverUrl(case-sensitive). Any other key fails,accessandmountincluded: who may read a document is the policy of@ApiDocs, in code, and configuration cannot relocate a document. The failure lists each unsupported key’s full path, sorted. - Unreadable
enabled. A string that is blank or made only of control characters cannot be read as a boolean, so it fails instead of keeping the decision of@ApiDocs. The failure names the application, its declaring interface, andapidocs.documents.<name>.enabled. enabled: true.trueis accepted only for an application whose declaring interface itself carries@ApiDocs; otherwise the failure names the application, its declaring interface, andapidocs.documents.<name>.enabled.
enabled is a tri-state. Absent and an explicit JSON null keep the decision of @ApiDocs, false
disables the document, and true only confirms a document that @ApiDocs already enables. A string
that is blank or made only of control characters is refused. A configuration entry never enables a document without @ApiDocs.
Keys elsewhere under apidocs are tolerated.
@ApiDocs shape
Section titled “@ApiDocs shape”For every active application whose declaring interface carries @ApiDocs, whether or not
configuration disables its document, startup re-checks, in one pass:
- that
policyis a validAccessPolicy: a public interface that extends onlyAccessPolicy, with no members, and whose direct requirements are valid (@PermitAlland@DenyAllexclusive of every other requirement, no empty or blank role, no blank scope, a canonical action). A failure names@ApiDocs.policy; - that
securitySchemeis empty when the policy is public, a blank string included, and is not blank for every other policy,@DenyAllincluded. A failure names@ApiDocs.securityScheme; - that the class file records a
policy. An interface whose@ApiDocsrecords none (a stale or partially built class file) has no policy to read: it fails closed with a message that says to recompile, is never served as a public document, and is not repaired by configuration.
The annotation processor enforces the same rules at compile time, so ordinarily only a
registration the processor did not produce, a declaration marked @NoAutoWire (which skips the
compile-time check), a stale class file, or a policy from a dependency whose version changed after
compilation can fail here; every such failure fails closed. All violations are reported in one
failure under Invalid @ApiDocs declarations:, sorted, each naming the application and its
declaring interface and the attribute. A failure never repeats a role, scope, action, or scheme
value. An inactive application is not checked.
Each enabled generated document needs an info with a non-blank title and version. A served
contract takes its info from the contract, and a configured info or an @OpenAPIDefinition on
its declaring interface is refused (see Served Contracts).
- From configuration.
apidocs.documents.<name>.info, when present, is used as a whole and replaces the annotation’sinfo: it publishestitle,description(when present), andversion, and no member of the annotation appears beside them. - From
@OpenAPIDefinition. Otherwise the@OpenAPIDefinition(info = ...)on the declaring type itself. The annotation is never inherited: one on a superclass or a superinterface is never read, even where@OpenAPIDefinitionis@Inherited. - Neither. Startup fails naming
apidocs.documents.<name>.info.
No default info is invented. Other members of @OpenAPIDefinition are not published in this release.
An annotated info publishes every member of @Info, @Contact, and @License that is set, in the
field order of the OpenAPI 3.1.1 objects. A blank string is unset, and an unset member is never
written:
- Info:
title,summary,description,termsOfService,contact,license,version, then its extensions. - Contact:
name,url,email, then its extensions; written only when one of them is set. - License:
name,identifier,url, then its extensions; written only when one of them is set.
An @Info that sets only title, version, and description therefore publishes exactly those
three. Member values are published as declared and never echoed in a message.
URLs (termsOfService, contact.url, license.url, every externalDocs.url, and
@ExampleObject.externalValue) are published exactly as declared and are not checked; never put
credentials, internal hosts, or non-http(s) schemes in them.
- Extensions. An
@Extensionof the@Info, its@Contact, or its@Licensewhose name starts withx-(case-sensitive) becomes a member of that name, written after the object’s other members. Its value is an object of its@ExtensionPropertynames and values; a property with a blank name is skipped, so an extension whose properties are all skipped publishes{}. A property withparseValue = trueholds its value parsed as JSON when the text is exactly one JSON value, and the text as a string otherwise. Extensions of one name merge in declaration order: the first keeps its position and a repeated property takes the later value. - Other extension names. An extension whose name does not start with
x-, a blank name included, is not published, and one warning per document names it (see Warnings). - License
identifierandurl. OpenAPI 3.1 makes them mutually exclusive, so an@Licensethat sets both fails startup naming the application, its declaring interface, and@OpenAPIDefinition.info.license, and echoing neither value. - Documented limit: a license without
name. OpenAPI 3.1 requiresnamein a License Object, but an@Licensethat setsidentifierorurlwithoutnameis published as declared, withoutname, and is not refused. Setnamewhenever the license sets anything.
serverUrl
Section titled “serverUrl”apidocs.documents.<name>.serverUrl, when present on an enabled generated document, must be one of
the forms below; on a document whose application serves its own contract it is refused whatever it
holds (see Served Contracts):
- an absolute
httporhttpsURI with a host (the scheme is compared case-insensitively); or - an absolute path that starts with a single
/and has no scheme or authority.
Anything else, a blank value included, fails startup naming apidocs.documents.<name>.serverUrl
without repeating the value. A valid serverUrl is published unchanged as the document’s
servers[0].url (see Root members), to every reader of the document. Never put
credentials, such as the user information of https://user:secret@host/, or any other secret in
it: the check does not refuse them, and the document publishes them as configured.
Startup Checks
Section titled “Startup Checks”When at least one document is enabled, the module checks every composition before any router is
created, and again as each JAX-RS mount publishes. Every failure stops the HttpVerticle before it
listens. A failure reported by the composition validator reaches the application as the
HttpVerticle’s IllegalStateException (Invalid mount configuration:) listing every violation,
sorted.
@ApiDocs values. The docs mount checks each enabled PROTECTED document, a policy that denies
everyone included, before it registers a route:
- its
securitySchememust be the scheme name of a registeredSecuritySchemeHandler; and - authentication enforcement must be installed.
Each violation names the application and its declaring interface, and all are listed together. Public documents are not checked. A protected document that passes has its routes installed through the security chain; a scheme whose handler registered no authentication handler, or a contributor that rejects the operation, then fails startup, and no document route remains (see Fail-closed startup).
Application mount. Each enabled document is matched to the JAX-RS mount whose application name equals the document’s name. The match never uses paths: a manually built mount at the application’s path does not match. A document with no such mount fails.
Mounts at or under the prefix. No JAX-RS mount may lie at or under apidocs.path. Move the mount
or choose another apidocs.path. Mounts that are not JAX-RS mounts, such as a UI mount, may lie under
the prefix. The documentation mount runs in the SYSTEM_FIRST phase, so it answers the exact
document URLs before every JAX-RS mount, every mount that sorts after it, AFTER_MOUNTS router
customizers, and API-scoped middleware; their routes are never reached at those URLs. ROOT-scoped
middleware and BEFORE_MOUNTS router customizers (the default customizer phase) run on the main router
before any mount, so they see and can answer the document URLs, as can another SYSTEM_FIRST mount that
sorts ahead of it (lower priority, or the same priority and a lower-sorting path such as /*). None of
these routes is checked for collisions; only published JAX-RS operations are.
Pattern mount paths. A JAX-RS mount whose path contains :, {, or } is refused while documents
are enabled when its literal part before the first such character is a prefix of apidocs.path, as
with /:tenant/* or /{tenant}/* under any prefix, or /api/{v}/* under /api/docs. Collision checks
cover literal mount paths only, so such a mount could reach the document URLs. A pattern mount whose
literal part does not prefix apidocs.path, such as /api/:tenant/* under /apidocs, is not refused.
Route collisions. The document URLs are <apidocs.path>/<name>/openapi.json and .yaml. A
published GET or HEAD route of a JAX-RS mount that can answer such a URL fails startup naming the
route, the operation, the mount, and the URL. The check matches the route as Vert.x routes it, regex
routes and path parameters included, and other methods never collide. Resolve a collision by choosing
an apidocs.path no such route can match, or by narrowing or removing the route.
Catch-all routes. A route that matches every path under its mount, such as GET /{path: .*} on a
mount at /, collides under every prefix. No apidocs.path avoids it, so the route must be narrowed or
removed.
Routes added by hooks and customizers. Routes that a RouterLifecycleHook or a MountCustomizer
adds to a JAX-RS router are not published, so they are neither published in a document nor checked for
collisions.
Reserved operation ids. For every enabled document, apidocs:<name>:json and apidocs:<name>:yaml
are reserved. A JAX-RS operation with one of these ids fails startup naming the operation and the
document’s application.
Shared global contract. An application whose selected request-validation strategy resolves
operations from the mount’s contract (openapi-contract) while that contract is the shared global
jaxrs.openapiPath fails startup if it has an enabled document. The shared contract file already is the
mount’s OpenAPI document, so no document is generated for it. Serve that file behind an access check at
least as strict as the most restricted mount it describes. An application whose contract location is
its own, from its annotation or its configuration, is not refused by this check: its contract is
served as its document (see Served Contracts).
Unvalidated composition. The docs mount refuses to create its router when the composition validator
has not checked it, which happens when the HttpVerticle is built without composition validators, such
as with its public five-argument constructor or by a subclass. Obtain the HttpVerticle from Dagger.
Referenced security schemes. While a document is assembled, every security scheme a published
operation requires must have a registered SecuritySchemeHandler whose openApiDescription()
returns a description of a kind this module renders; otherwise the document fails naming the
scheme and the first operation that requires it (see
Fail-closed publication).
The hook checks run for every JAX-RS mount of the composition, documented or not, in the order reserved ids, route collisions, shared contract. The first check with a violation fails the mount, listing all of that check’s violations.
Mount Customizers
Section titled “Mount Customizers”Every MountCustomizer whose matches accepts the docs mount’s fixed metadata is applied to the docs
router, as to any mount. Customizers are applied after the documentation router is created, so a route
a customizer adds without an explicit order runs after the document routes. A protected document’s route
never continues, so such a route never runs for a served protected document, nor for a public document
that is answered. A customizer route that must run first has to call
route().order(Integer.MIN_VALUE) before it adds its handler, because Vert.x refuses an order
change once the route has handlers. Such a route runs for every request under the prefix, so its
handler must pass the request on with next() unless it ends the request.
A customizer that matches every mount and adds a handler that ends every request, instead of passing it
on, also ends every request the docs mount does not answer. Requests under the prefix then stop falling
through to later mounts. Keep such handlers pass-through, or have matches exclude the mount id
apidocs.
Warnings
Section titled “Warnings”Every warning of the module is logged at WARN on logger
dev.vertique.rest.openapi.docs.DocumentWarnings and starts with apidocs.documents.<name>. No
warning carries schema text, an annotation value other than the extension names it lists, or a
configuration value other than the document name, a mount path, and, for a served contract, its
contract location (see Warnings of a served contract). Each is logged at most once per component, so a second composition
or server instance of the same component does not repeat it.
Controls that skip the document routes
Section titled “Controls that skip the document routes”The module logs one such warning per enabled document. It is logged when the documentation module’s composition checks pass, before any router is created, so it can appear even when another startup check (including another module’s composition validator in the same pass, or a later check) fails the deployment.
The warning starts with apidocs.documents.<name> and the described mount’s path, and lists the
mount-scoped controls that apply to the application’s mount but not to the document routes, each as
<kind> <class name>:
MountCustomizer: matches the application’s mount but not the docs mount;API middleware: a middleware ofAPIscope other than the framework’s content-type middleware;RouterLifecycleHook: every hook;RequestInterceptor: every interceptor that overridesbeforeRequest.
The last three apply only to a mount that has resources. Document requests bypass these controls,
while main-router middleware still applies. For a public document the warning also states that no
OperationHandlerContributor runs for the document routes. A document with no such control logs
nothing.
Metadata warnings
Section titled “Metadata warnings”Four warnings report Swagger annotation content the document does not publish. Unlike the warning
above, they are held back while the document is assembled and logged only once it is built
successfully, so a document that fails startup logs none of them. They are logged in document order:
the info warning first, then each operation’s warnings in the order the document lists the
operations, its input warnings before its response warning. Hidden operations and hidden inputs
cause none.
- Unpublished
infoextensions. One per document, when the annotatedinfodeclares an extension whose name does not start withx-(seeinfo). It names the application and its declaring interface and lists every such name, sorted, a blank name as<unnamed>; it quotes no property value:apidocs.documents.<name>: application '<name>' (declared by <binary name>) has @OpenAPIDefinition.info extensions whose names do not start with 'x-', which are not published: <names>. - Ignored schema members. One per operation that declares any (see
Ignored schema members), naming every such member as
<annotation path> on <input>, for example@Parameter.schema.maxLength on query parameter q:apidocs.documents.<name>: operation '<id>' at mount '<mount path>' declares schema members the document does not publish, because each input's canonical schema stays authoritative: <members>. - Requiredness that cannot be determined. One per parameter or form field that declares
@Parameter(required = true)while the runtime cannot tell whether it rejects a missing value (see Agreement with the runtime):apidocs.documents.<name>: operation '<id>' at mount '<mount path>' declares @Parameter.required on <input>, but whether the runtime rejects a missing value cannot be determined, so the document does not mark it required. - Omitted response attributes. One per operation whose declared responses set attributes the
document does not publish, naming each as
<attribute> on status <status>(see Omitted response attributes):apidocs.documents.<name>: operation '<id>' at mount '<mount path>' declares response attributes the document does not publish: <attributes>.
Each message is one line; it is wrapped here for reading.
Operations that restrict callers
Section titled “Operations that restrict callers”A PUBLIC document is served without authentication, so it shows anyone what its restricted
operations accept. When it lists any published operation that restricts callers, one warning names
them all:
apidocs.documents.<name>: the public document of application '<name>' at mount '<mount path>' listsoperations that restrict callers, and it is served without authentication: <entries>- What restricts callers. A restrictive effective policy (
@RolesAllowed,@Authorized, or@DenyAll), any security requirement, or a required@RequiresActionaction. - Entries. Each such operation as
<METHOD> <path> (<operationId>), joined by,, in document order (path keys in natural order, then methods), for exampleGET /admin (adminReport), GET /scopeless (scopelessGet). Hidden operations are not listed. - Names nothing secret. The warning names no role, scope, action, or claim.
- Annotation-declared only. The warning sees only restrictions declared by annotation: security annotations, requirement sets, and required actions. Access checks added by router hooks, middleware, or interceptors are not detected.
- Publication proceeds. The document is served as usual.
- Served contracts. For a document that is the application’s own contract, the entries come
from the mount’s operations that are not hidden, never from the contract’s
security(see Warnings of a served contract). - When. Held back like the metadata warnings and logged last, after every other warning of the document, only once the document is written; once per document and component. A protected document never logs it, nor does a public document whose operations restrict no caller.
Failures, Constraints, and Common Mistakes
Section titled “Failures, Constraints, and Common Mistakes”Startup failures
Section titled “Startup failures”Every failure below stops startup. Each message names what is wrong and never echoes contract content or configuration values (except the contract location of a served contract with an unsupported extension), schema text, references, pattern text, redaction locations, reserved names, or annotation values. Messages about the document’s content name operation ids, rendered paths, input names, component keys, tag names, annotation attributes, the class of the bound schema source, the declaring type and member of a hidden member, response statuses and header names, and the declaring type, member, serialized name, and schema name of a renamed output member, and security scheme names with the method and path key of the operation requiring them. Two quote what the developer declared: an invalid response status is quoted as declared, and a renamed output member’s names are quoted.
| Condition | Failure |
|---|---|
apidocs is not a JSON object |
ConfigurationException from the section navigation |
apidocs.enabled is present and not a JSON boolean |
ConfigurationException naming apidocs.enabled |
An apidocs.documents key breaks the name grammar, or names no declared application |
ConfigurationException naming apidocs.documents.<name> and the rule |
An entry holds a key other than enabled, info, or serverUrl |
ConfigurationException naming the application, its declaring interface, and each unsupported key’s path |
An entry sets enabled to a string that is blank or made only of control characters |
ConfigurationException naming the application, its declaring interface, and apidocs.documents.<name>.enabled, ending it must be true or false, or be left out to keep the @ApiDocs decision |
An entry sets enabled: true for an application without @ApiDocs |
ConfigurationException naming the application, its declaring interface, and apidocs.documents.<name>.enabled |
| A blank key, or an entry that is not a JSON object | The keyed-collection parser’s own ConfigurationException, raised before any check above; it says the entry has a blank key, or that the entry '<key>' must be a nested JSON object, and names the key |
The @ApiDocs of an active application breaks its shape rules: an invalid policy, a scheme on a public policy, no scheme on any other policy |
RestConfigurationException under Invalid @ApiDocs declarations: listing every violation, sorted, each naming the application, its declaring interface, and @ApiDocs.policy or @ApiDocs.securityScheme |
The class file of an active application’s interface records an @ApiDocs with no policy (a stale or partially built class file) |
RestConfigurationException in the same listing, naming the application and its declaring interface and saying to recompile; no document is served for it, and none is public |
A document is enabled and apidocs.path breaks a rule above |
ConfigurationException naming apidocs.path |
An enabled document has no info, or a blank title or version |
ConfigurationException naming the application, its declaring interface’s binary name, and apidocs.documents.<name>.info, .info.title, or .info.version; a blank title or version in @OpenAPIDefinition(info) names @OpenAPIDefinition.info |
An enabled document takes its info from @OpenAPIDefinition and its @License sets both identifier and url |
ConfigurationException naming the application, its declaring interface’s binary name, @OpenAPIDefinition.info.license, and apidocs.documents.<name>.info; neither value is echoed |
An enabled document has an invalid serverUrl |
ConfigurationException naming the application, its declaring interface, and apidocs.documents.<name>.serverUrl |
An enabled PROTECTED document, one whose policy denies everyone included, names a securityScheme no registered handler has, or authentication enforcement is not installed |
RestConfigurationException whose violations each start Application '<name>' (declared by <interface>): , naming @ApiDocs.securityScheme and the scheme, or @ApiDocs.policy and the missing authentication enforcement; several are listed under Invalid @ApiDocs values:; raised before any document route is registered |
An enabled PROTECTED document’s scheme handler registered no authentication handler, or a contributor rejects its operation while the route is built |
RestConfigurationException starting Protected API document of application '<name>' (access policy: @ApiDocs on <interface>); every document route is removed again, so the documentation router has no route |
An enabled document has no JAX-RS mount of its application name, a JAX-RS mount lies at or under apidocs.path, or a JAX-RS pattern mount path can reach the prefix |
IllegalStateException from HttpVerticle (Invalid mount configuration:) listing every violation, raised before any router is created |
The docs mount is hosted by an HttpVerticle built without composition validators |
RestConfigurationException naming the docs mount, the cause, and the remedy (obtain HttpVerticle from Dagger) |
A JAX-RS operation uses a reserved id apidocs:<name>:json or apidocs:<name>:yaml |
RestConfigurationException naming the operation id, its method and template, the mount, and the application |
A GET or HEAD route of a JAX-RS mount can answer a document URL |
RestConfigurationException with one line per route and URL, naming both |
| A documented application resolves operations from the shared global contract | RestConfigurationException naming the application, the mount, and the strategy |
An application that serves its own contract has a configured apidocs.documents.<name>.info or .serverUrl, or an @OpenAPIDefinition on its declaring interface (see Served Contracts) |
ConfigurationException naming the application, its declaring interface, and the configuration path or @OpenAPIDefinition, stating that the served document is never rewritten; no value is echoed |
| Two enabled documents serve their own contracts from one location after path normalization, or a contract location cannot be parsed as a path | IllegalStateException from HttpVerticle (Invalid mount configuration:) naming the applications, or the application and the setting the location comes from; never the location |
A served contract’s location has an extension other than .json, .yaml, or .yml (any letter case), or none |
RestConfigurationException naming the application, the setting, and the location, raised before the location is read |
| A served contract cannot be read, or is not valid JSON or YAML | RestConfigurationException naming the application and the setting; no cause, and no file-system or parser text |
A served contract is not OpenAPI 3.0 or 3.1, has a non-local reference, an operationRef or $id, an unresolved or cyclic reference, a missing or repeated operation id, an unrouted id, an unbound or undescribed operation, a described hidden input, or a form schema that is not a plain object schema (see Startup checks of a served contract) |
RestConfigurationException starting The served contract of application '<name>' is refused: , listing every violation, sorted, by operation id and JSON Pointer; no contract content |
| Two operations of a documented application render to equivalent paths with different variable names, or with the same method | RestConfigurationException starting Application '<name>' (declared by <binary name>) at mount '<mount path>', naming both routes by method, operation id, and rendered path, and stating that routes at one path must use the same variable names and differ in method |
| An operation of a documented application hides a path parameter (see Hidden inputs) | RestConfigurationException with the same start, naming the operation and the parameter, and stating that a path parameter cannot be hidden |
| An operation of a documented application binds two visible inputs with the same name and location | RestConfigurationException with the same start, naming the operation, the input name, and the location, and stating that a document describes one parameter per name and location |
A published request body of a documented application carries no redaction manifest matching its content, such as one a custom or decorating OperationSchemaSource built, replaced, or edited (see Reserved-name redaction) |
RestConfigurationException with the same start, naming the operation and the class of the bound schema source |
| A published request body carries a redaction manifest that does not resolve in its schema | RestConfigurationException with the same start, naming the operation and the class of the bound schema source |
A captured parameter or form-field schema of a documented application holds the propertyNames keyword |
RestConfigurationException with the same start, naming the operation and the parameter’s location and name, or the form field |
A published request body’s type is described with a member or type carrying @Hidden or @Schema(hidden = true) (see Hidden members of a request body) |
RestConfigurationException with the same start, naming the operation, the member or type, its marker, and the fix |
| A published request body’s type cannot be inspected for hidden members | RestConfigurationException with the same start, naming the operation; the cause is not echoed |
| A captured schema of a visible input of a documented application holds a refused construct (see Refused constructs) | RestConfigurationException with the same start, naming the operation, the input, the construct, and the JSON Pointer of the offending keyword; no value, reference, or schema text |
| A Swagger annotation of a visible operation of a documented application contradicts how the runtime binds it (see Agreement with the runtime) | RestConfigurationException with the same start, naming the operation and the attribute, and for an input attribute the input and the runtime fact it contradicts; no annotation value |
A @Parameter, @RequestBody, or @ExampleObject of a visible operation sets ref |
RestConfigurationException with the same start, naming the operation and the attribute with its input, and stating that the document declares no reusable parameters, request bodies, or examples |
An @ExampleObject of a visible input has a blank name, a name used twice in one array, or both value and externalValue (see Examples) |
RestConfigurationException with the same start, naming the operation, the attribute, and where it is declared |
Two @Tag declarations of one name set a different description or externalDocs (see Tags) |
RestConfigurationException with the same start, naming the tag, the member, and both operations, never either value |
A declared @ApiResponse status is not default, a code from 100 to 599, or an uppercase range key from 1XX to 5XX (see Declared responses) |
RestConfigurationException starting apidocs.documents.<name>: and then the same start, naming the operation and quoting the declared status |
| The method level or the class level declares one response status twice | RestConfigurationException with the apidocs.documents.<name>: start, naming the operation, the status, and the level |
@ApiResponse.useReturnTypeSchema is true on a return type that is not inferable (see A declared success status without content) |
RestConfigurationException with the apidocs.documents.<name>: start, naming the operation, the status, and the attribute |
| One status declares a media type twice, or one header name twice, ignoring ASCII letter case (see Response content and headers) | RestConfigurationException with the apidocs.documents.<name>: start, naming the operation, the status, and @Content.mediaType or @Header.name; neither value is echoed |
An @ExampleObject in @Content.examples has a blank or repeated name, both value and externalValue, or a ref |
RestConfigurationException with the apidocs.documents.<name>: start, naming the operation, the attribute, and the status |
| A published output type cannot be generated or inspected under the operation’s output profile (see Refusals of output types) | RestConfigurationException with the apidocs.documents.<name>: start, naming the operation, the content or header schema, and the status (200 for the inferred type); the generator’s exception is the cause; no schema text |
A published output type describes a member under a @Schema(name) that differs from its serialized name |
RestConfigurationException with the apidocs.documents.<name>: start, naming the operation, the status, the declaring type, the member, the serialized name, and the schema name |
A published output type describes a member or type carrying @Hidden or a @Schema(hidden = true) the output generator ignores |
RestConfigurationException with the apidocs.documents.<name>: start, naming the operation, the status, the member or type, its marker, and the fix |
| A generated output schema holds a refused construct | RestConfigurationException with the same start, naming the operation, the output schema and its status, the construct, and the JSON Pointer |
| Two components of a document would have the same key | RestConfigurationException with the same start, naming the input or output of both components and the key; when either is a relocated definition, it says one component instead of the key, and states that component keys replace every character outside [A-Za-z0-9._-] with _ |
A published operation requires a security scheme whose registered handler returns no openApiDescription() (see Fail-closed publication) |
RestConfigurationException with the apidocs.documents.<name>: start, naming the first operation requiring it by id, method, and path key, and the scheme, and stating the fix; no description content or configuration value |
| A published operation requires a security scheme no registered handler provides | RestConfigurationException with the same start, naming the operation and the scheme; in a deployment the route registration refuses the operation first |
| A published operation requires a security scheme that more than one registered handler provides | RestConfigurationException with the same start, naming the operation, the scheme, and the handlers’ binary class names, sorted; provide the scheme from exactly one handler |
| A referenced scheme’s handler returns a description of a kind this module does not know | RestConfigurationException with the same start, naming the operation, the scheme, and the description’s class |
| A later server instance publishes a different mount or operation than the stored document | RestConfigurationException naming the application, its declaring interface, its mount path, and the first differing operation id |
| A mount of a documented application is built outside a Vert.x context | RestConfigurationException naming the application |
Entries of applications without @ApiDocs, and entries that switch a document off, are still checked
for their name, their application, their keys, and an unreadable enabled. The info and serverUrl of a document are checked
only when it is enabled. A value of the wrong type inside an entry is rejected by the canonical parser,
and its message can quote that value.
Common mistakes
Section titled “Common mistakes”- Annotating without an
info.@ApiDocsalone does not start: every enabled generated document needs aninfowith atitleand aversion, from@OpenAPIDefinition(info)on the declaring interface or fromapidocs.documents.<name>.info(seeinfo). Disable a document withapidocs.documents.<name>.enabled: falseinstead of removing the annotation when the configuration is not ready. - Keying a document entry by path instead of name. Entries are keyed by the
@RestApplicationname. An entry that names no declared application fails startup, even when it only setsenabled: false. - Expecting
enabled: trueto enable a document. Only@ApiDocsenables.enabled: truefor an application without@ApiDocsfails startup; the entry can only switch a documented application off. - Putting
accessormountin an entry. Entries accept onlyenabled,info, andserverUrl; access is declared by the policy of@ApiDocsin code. - Mounting a JAX-RS mount or a catch-all route under the prefix. It fails startup. Choose another
apidocs.path, or move, narrow, or remove the mount or route. - A security handler without a description. A published operation requiring a scheme whose
handler returns no
openApiDescription()fails startup; return a description, or disable the document. - Expecting roles or actions in the document.
@RolesAllowedroles,@RequiresActionactions,@PermitAll,@DenyAll, and application checks are never published; only security requirements and their scopes are. A public document listing restricted operations logs a warning instead. - Expecting an unreferenced scheme to be published.
securitySchemesholds only the schemes a published operation requires; a registered handler nothing references, or only hidden operations reference, is left out. - Putting credentials or internal hosts in a scheme description. Descriptions, OAuth2 flow URLs, scope names and descriptions, API key names, and the OpenID Connect URL are published exactly as the handler supplies them and are not checked.
- Setting targeted cache headers by default with a protected document. Only
Cache-Controlis replaced on a protected document.CDN-Cache-Control,Surrogate-Control,X-Accel-Expires, orExpiresset throughjaxrs.defaultHeaderssurvive, so a CDN or proxy that obeys them could store the document; do not configure them while a protected document is enabled. - Expecting a response for
Response,CompletionStage, or a producer-bound type. Their content is decided at runtime, so they publishdefaultonly; declare the responses with@ApiResponse. ReturnFuture<T>instead ofCompletionStage<T>to haveTinferred. - Expecting a schema for a
Stringreturn. AStringis raw text under every media type,application/jsonincluded, and publishes no schema. - Expecting error responses to be inferred. No
4XX,5XX, or error body is ever inferred; declare them with@ApiResponse. - Declaring a
404and losing the inferred200. A declared set replaces the inferred response entirely; also declare the success status. A success status declared withoutcontentkeeps the inferred content. - Comparing with a build-time generated document. A success status declared without
contentkeeps the inferred schema here, while swagger-core publishes it without content unlessuseReturnTypeSchemaistrue. - Declaring responses in
@Operation(responses = ...). They are not read; use@ApiResponseon the method or the class. - Writing a range key in lowercase.
2xxfails startup; write2XX. - Declaring one status differently on an implementation and its interface method. Both are the method level, so startup fails; declare it once.
- Hiding an output property with
@Hidden. The output generator ignores@Hidden, so startup fails; declare@Schema(hidden = true)on the property’s own field or getter. - Renaming an output property with
@Schema(name = ...). The response still carries the serialized name, so startup fails; rename it on the wire with@JsonProperty, or remove the@Schema(name). - Hiding a response header with
@Header(hidden = true). It is not honored: the header is published and the attribute is warned. Remove the@Headerinstead. - Hiding a response body with
@Schema(hidden = true)on its@Content. It is not honored: the implementation is published in full and the attribute is warned. Remove the content declaration or hide the operation. - Expecting a response schema’s
exampleon the Media Type Object. It stays beside the$refin the Schema Object; use@Content.examplesfor media-type examples. - Expecting a server URL from the request.
servers[0].urlis the configuredserverUrlor the mount path; behind a proxy that changes the path, configureserverUrl. - Putting credentials in
serverUrl. It is published exactly as configured, user information included; keep credentials and other secrets out of it. - Putting credentials, internal hosts, or other schemes in a documentation URL.
termsOfService,contact.url,license.url, everyexternalDocs.url, and@ExampleObject.externalValueare published exactly as declared and are not checked; use publichttp(s)URLs only. - Hiding a path parameter. It fails startup; a document describes every path variable.
- Expecting a hidden input to stop binding. Hiding changes the document only; the request still binds and validates the input.
- A schema source that replaces or edits the generated body schema. With an enabled document it fails startup; return the generated body schema and its redaction manifest unchanged.
- A schema source that describes a different type than the bound body type. The hidden-member
check inspects the bound type only, so
@Hiddenmembers of the other type are not refused and publish; return the generated description of the bound type unchanged. - Hiding a body property with
@Hidden. The input generator ignores@Hidden, so startup fails; declare@Schema(hidden = true)on the property’s own field or getter. - Expecting
required: false. An input that is not certainly required carries norequiredmember at all. - Expecting schemas for composite-bean fields. They are unenforced and publish
{}or their@DefaultValueonly. - Using
$id, anchors, or nested$defsin an input schema. A documented application refuses them at startup; keep local definitions in the root$defsand reference them by fragment. - Expecting routes added by a hook or customizer to be checked. They are neither published nor collision-checked.
- A customizer that ends every request. A match-all customizer whose handler does not pass the request on stops fall-through under the prefix.
- Putting
@ApiDocson a superinterface. Only the@RestApplicationdeclaring interface is read; the annotation processor rejects it on a superinterface. - Expecting an authenticated-only policy to restrict a protected document. A policy that is one
@Authorizedwithout scopes admits any caller the scheme authenticates, and an INFO line says so at startup; use@RolesAllowed, scopes, or@RequiresActionto restrict who may read it. - Expecting configuration to change a document’s access. Only the policy of
@ApiDocson the declaring interface decides it; use a policy that is exactly one@PermitAllonly where the document may be read by any caller. - Giving a public policy a
securityScheme, or another policy none. A public document names no scheme, not even a blank one, and every other policy,@DenyAllincluded, needs a registered scheme; both fail compilation and startup. - Mixing
@PermitAllwith another requirement in a policy. The policy is invalid, not public, and so is a policy that declares no direct requirement. - Running a stale class file. An interface whose class file records an
@ApiDocswith nopolicyfails startup closed until it is recompiled. - Expecting the application’s error handling on a protected document. Its
401,403,404,503, and500answers end on the document route’s own failure handler; exception mappers, error interceptors, and later mounts never see them. - Expecting a
404or405from the docs mount for a public document. The mount passes every public-document request it does not answer to the later mounts; a trailing slash,POST, or unknown name is answered by them. When no later mount claims the request, the router answers it:405for another method on a document URL (itsAllowheader namesGETandHEAD),404for an unknown name or a public document’s trailing-slashGETorHEAD. A protected document’s trailing-slash variant ends with404once the chain has passed. - Publishing different content from different server instances. Instances of one component are compared, and a difference fails the later instance’s startup.
- Omitting the module and expecting silence. An application whose interface carries
@ApiDocsbut whose component does not listOpenApiDocsModuleserves no document, andvertique-rest-jaxrslogs one INFO line per such application saying so. - Marking a parameter
@Parameter(required = true)without enforcing it. On an input with@DefaultValue, without a constraint annotation, or with no bean validator bound, the runtime accepts a missing value, so startup fails. Add a null-rejecting constraint such as@NotNull, or remove the attribute. - Expecting
@Parameter(required = true)to addrequired. The document writesrequiredfrom what the runtime enforces; when that cannot be determined, the attribute is not published and a warning names the input. - Narrowing a schema with
@Parameter(schema = @Schema(...)). Members such asmaxLength,format, orpatternare not published; the captured schema stays as the runtime validates it, and a warning names them. Put the constraint on the input itself. - Hiding an input with
@Parameter(schema = @Schema(hidden = true)). It does not hide the input; it is warned as an ignored member. Use@Parameter(hidden = true). - Spelling a query, path, cookie, or form-field name in another case. Only header names ignore
ASCII letter case;
@Parameter(name = "ID")on a query parameter bound asidfails startup. - Expecting a hidden operation to stop routing.
@Hiddenand@Operation(hidden = true)change the document only; the route still answers, and its id and route are still checked for reserved ids and document-URL collisions. - Expecting
@Operation.tagsalone to add a root tag. Only@Tagon the method or class adds a root tag with its description; an@Operation.tagsname appears on the operation only. - Declaring one tag with different descriptions. Every
@Tagof one name that sets adescriptionorexternalDocsmust set the same value, or startup fails; set it once. - A license with both
identifierandurl, or withoutname. Both together fail startup. A license withoutnameis published as declared, although OpenAPI 3.1 requiresname; set it. - Expecting annotated
infomembers beside a configuredinfo. A configuredinforeplaces the annotation as a whole. - Configuring
infoorserverUrlfor an application that serves its own contract. Startup fails; the contract is served as written, so put theinfoandserversin the contract. - Expecting
jaxrs.openapiPathto be served. The shared contract is never a document; give the application its own@RestApplication.openapiPathorjaxrs.applications.<name>.openapiPath. - Expecting a served contract to be enriched or redacted. It is served as parsed: no inferred responses, no security schemes, no validation disclosure, and nothing removed. Hidden operations and hidden inputs are not stripped; a contract that describes a hidden input fails startup.
- Leaving a stray contract file in the working directory. For a relative location, a
working-directory file shadows the packaged classpath resource, both for the document and for
openapi-contractvalidation. A WARN names the location; keep the working directory controlled. - Pointing two applications at one contract file. Each served document needs a contract location of its own.
- Splitting a contract across files. Only references within the document (
#/...) are accepted; bundle external references into the one file. - Leaving a routed operation out of the contract, or describing one that is not routed. Every
routed operation that is not hidden must be described with its own
operationId, method, and path, and everyoperationIdoutside webhooks and callbacks must name a routed operation. - Expecting the contract’s schemas to be checked against the code. Under
web-validationthe contract plays no part in validation and nothing compares its schemas with what the runtime enforces; keep them in step. - A malformed
serversURL underopenapi-contract. Startup fails: the strategy cannot load the contract. A relative URL, including the mount path theserverswarning expects, loads. - Expecting a contract file edit to be served without a restart. The contract is loaded once per component; a later edit is not served.
Module Dagger Bindings
Section titled “Module Dagger Bindings”OpenApiDocsModule provides:
| Binding | Notes |
|---|---|
| The enabled documents | Internal type, @Singleton; parses the apidocs section through the canonical ConfigParser (when apidocs.enabled is false, without parsing the rest of the subtree), decides which applications have a document, and runs the configuration checks above. The parsed section itself is not bound |
@ElementsIntoSet Set<MountCompositionValidator> |
The composition validator when at least one document is enabled, otherwise an empty set; contributes to the set HttpVerticle runs before it creates any router |
@ElementsIntoSet Set<MountPublicationHook> |
The publication hook when at least one document is enabled, otherwise an empty set; contributes to the set vertique-rest-jaxrs declares |
@ElementsIntoSet Set<RouterMount> |
The docs mount when at least one document is enabled, otherwise an empty set; unscoped, so every composition builds its own mount |
| The documentation warnings | Internal type, @Singleton; holds the once-per-component guard of the startup warnings |
| The document store | Internal type, @Singleton, one per component; holds the documents keyed by application name and is shared by every HttpVerticle instance |
ApiDocsModuleInstalled |
Bound whenever the module is listed, whatever the configuration, so vertique-rest-jaxrs does not log that no documentation route is published for @ApiDocs applications, even with apidocs.enabled false |
The module requires @VertxConfig JsonObject, ConfigParser (from ConfigParsingModule),
JaxRsConfig, RestApplications, and SyntheticOperationInstaller (all bound by RestModule in
dev.vertique:vertique-rest-jaxrs),
and the component’s multibound sets of RequestValidationStrategy, SecuritySchemeHandler,
MountCustomizer, Middleware, RouterLifecycleHook, and RequestInterceptor, plus an optional
AuthEnforcementCapability, the optional OperationSchemaSource that RestModule declares, the
multibound Set<ResponseProducerBinding<?>> (from dev.vertique:vertique-rest-core), which
decides which return types publish default, and the JsonMapperProfileRegistry from
JsonRuntimeModule (in dev.vertique:vertique-json), which RestModule includes.
Developer Guide
Section titled “Developer Guide”Task-oriented guidance for adopting runtime documents. Each subsection points to the reference
section above, or to another module’s reference, that holds the full rules. The examples
examples/vertique-example-apidocs/ and examples/vertique-example-services-codegen/ show the
patterns in a running application.
Declaring named applications
Section titled “Declaring named applications”A document belongs to a declared application, so start with @RestApplication (from
dev.vertique:vertique-rest-core) on an interface, as Getting Started shows:
name. Matches[a-z0-9][a-z0-9_-]{0,63}and is neithernonenornull. It names the document, its URLs, and itsapidocs.documents.<name>andjaxrs.applications.<name>entries, so keep it stable: a renamed application moves its document URLs, and an entry under the old name fails startup.path. The mount path, normalized at compile time:/api/public/and/api/public/*both become/api/public.- Membership. Exactly one of a non-empty
resourceslist anddiscover = true, which only the sole declared application may use. openapiPath. Names the application’s own contract, andjaxrs.applications.<name>.openapiPathoverrides it. Either makes the document that contract (see Serving an application’s own contract).
The annotation processor of dev.vertique:vertique-codegen-jaxrs checks each declaration at
compile time and emits one registration per interface into the generated
GeneratedJaxRsResourcesModule; list that module and RestModule in the component. At startup,
before the server listens, a name declared twice in the component (active or not), a
discover = true application beside another declaration, a jaxrs.applications.<name> entry that
names no declaration, and two active applications whose paths overlap each fail. The full rules are
in the “@RestApplication” section of dev.vertique:vertique-rest-jaxrs and the “REST Application
Declarations” section of dev.vertique:vertique-codegen-jaxrs.
Migrating from Application subclasses
Section titled “Migrating from Application subclasses”A jakarta.ws.rs.core.Application subclass is not registered. The build warns once for each
concrete subclass that its @ApplicationPath and getClasses() have no effect, and where its
resources are served instead: at jaxrs.basePath on the legacy default mount when the component
declares no @RestApplication, otherwise only where a declared application lists them. A build that
treats warnings as errors fails on it, and @NoAutoWire on the subclass silences it.
- Port it. Declare a
@RestApplicationinterface with the same path that lists the subclass’s resources, or setsdiscover = truewhen it is the sole application, then delete the subclass. The “Porting a JakartaApplicationSubclass” section ofdev.vertique:vertique-codegen-jaxrsshows the before and after. - Check the perimeter. Until it is ported, the resources no longer answer under the old
@ApplicationPathprefix, so gateway routes, allowlists, and other rules keyed on that prefix stop matching them.
Documents for the legacy default mount. The default mount never publishes a document. To
document an application that uses it, declare one sole discovery application at the same place, as
examples/vertique-example-services-codegen/ does (members elided):
@RestApplication(name = "services", path = "/", discover = true)@ApiDocs(policy = ServicesApi.DocsPolicy.class)@OpenAPIDefinition( info = @Info( title = "Example Services Codegen API", version = "0.1.0", description = "…"))public interface ServicesApi {
@PermitAll public interface DocsPolicy extends AccessPolicy {}}Its routes stay where they were under three constraints:
pathis yourjaxrs.basePathwithout/*, so/for the default/*.- A root application conflicts with every other JAX-RS mount, application or hand-built, so it must be the only one.
- Declaring any application switches composition to explicit mode. A manual
@JaxRsResourcescontribution is routed only when an application selects it, startup warns about resources no application selects, and each application mount warns about operations with no explicit security policy. Declare the policy on public resources with@PermitAll, as the API documentation example’sCatalogResource(examples/vertique-example-apidocs/) does (members elided):
@PermitAll@Path("/items")@Produces(MediaType.APPLICATION_JSON)public class CatalogResource { // …}The services example (examples/vertique-example-services-codegen/) leaves the policy of its
resources implicit, so its startup logs the “no explicit security policy” warning for them, which is
expected there.
The “Explicit mode” and “Explicit security policy” sections of dev.vertique:vertique-rest-jaxrs
list every warning.
Enabling documents with @ApiDocs
Section titled “Enabling documents with @ApiDocs”- List
OpenApiDocsModulein the component. - Put
@ApiDocson the declaring interface with its policy, which only code decides: a policy interface that is exactly one@PermitAllfor a public document, or any other policy with asecurityScheme. A policy that is one@Authorizedwithout scopes admits any caller the scheme authenticates, and an INFO line says so (see Access:PUBLICandPROTECTED). - Give the document an
infofrom@OpenAPIDefinition(info)on the declaring interface or fromapidocs.documents.<name>.info; the example uses one of each (seeinfo).
@ApiDocs guards the document routes only and is not API protection: secure the operations
themselves, as the example’s ManagementResource does (members elided):
@Path("/")@Produces(MediaType.APPLICATION_JSON)@SecurityRequirement(name = "bearerAuth")@RolesAllowed("admin")public class ManagementResource { // …}Configuration describes or switches off documents but never changes access: apidocs.enabled: false
turns every document off, apidocs.documents.<name>.enabled: false turns one off, and an entry may
set info and serverUrl; access and mount are refused (see Configuration).
When the docs artifact is on the classpath but the component does not list OpenApiDocsModule,
dev.vertique:vertique-rest-jaxrs logs one INFO line per active application carrying @ApiDocs,
stating that no documentation route is published for it.
Operation ids and application identity
Section titled “Operation ids and application identity”Once any application is declared, operation ids are unique across all JAX-RS mounts, with one
exemption: the same method of the same resource class keeps its id in every application that mounts
it. Two distinct resource methods that share an id across mounts fail startup. This rule is kept
until a later change scopes operation ids per mount (and, under a strategy that resolves operations
from the mount’s contract, such as openapi-contract, across the mounts sharing one contract
location) and logs a startup WARN naming the applications that share an id; until then, no such
WARN is logged.
Code that keys state or decisions on the operation id alone can already conflate one resource method mounted by several applications. Key it on the application as well:
- Where the name is.
RestOperationDescriptor.applicationName(),MountMeta.applicationName(), andOperationContext.operation().applicationName(), allnullfor the legacy default mount and for hand-built mounts. A protected document’s synthetic operation reports the documented application. - In an interceptor. Key state on
ctx.operation().applicationName()plus the operation id. The “RequestInterceptor,OperationInterceptor,ErrorInterceptor” section ofdev.vertique:vertique-rest-coreshows the pattern. - Metrics and traces. See the “
vertique.rest.server.requests— Timer” section ofdev.vertique:vertique-micrometer-restand the span attributes ofdev.vertique:vertique-opentelemetry-rest.
Choosing the documentation prefix
Section titled “Choosing the documentation prefix”Documents are served under apidocs.path, /apidocs by default (see Configuration
for its rules). Startup fails when a published GET or HEAD route of a JAX-RS mount, typically a
root application’s, can answer a document URL, and when a JAX-RS mount lies at or under the prefix
(see Startup Checks):
- A colliding route. Choose an
apidocs.paththat no route matches, or narrow the route. - A route that matches every path, such as
GET /{path: .*}on a root mount, collides under any prefix: narrow or remove it before enabling documents. - Routes added by a
RouterLifecycleHookor aMountCustomizerare neither published nor checked for collisions; check them yourself against the document URLs.
Serving a UI under the prefix
Section titled “Serving a UI under the prefix”The docs mount answers only the exact document URLs and passes every other request under the prefix
on, so an application RouterMount in the default phase can serve a UI there (see
What the mount answers, and what it passes on). The
example contributes its page this way, from ApiDocsUiMount in
examples/vertique-example-apidocs/ (members elided):
@Provides@IntoSetstatic RouterMount apiDocsUiMount() { return new ApiDocsUiMount();}static final String MOUNT_PATH = "/apidocs/ui/*";// …public Future<Router> createRouter(Vertx vertx) { return vertx.executeBlocking(ApiDocsUiMount::readPage).map(page -> { Router router = Router.router(vertx); router.route("/").method(HttpMethod.GET).method(HttpMethod.HEAD).handler(ctx -> serve(ctx, page)); return router; });}
private static void serve(RoutingContext ctx, byte[] page) { HttpServerResponse response = ctx.response() .putHeader("Content-Type", CONTENT_TYPE) .putHeader("Content-Security-Policy", CONTENT_SECURITY_POLICY) .putHeader("Content-Length", Integer.toString(page.length)); if (ctx.request().method() == HttpMethod.HEAD) { response.end(); } else { response.end(Buffer.buffer(page)); }}Startup then logs the framework’s overlap warning for the two mounts, which is expected here:
Mount path '/apidocs/*' (priority=<n>) is a prefix of '/apidocs/ui/*' (priority=<n>); requests matching both will be handled by the first-mounted. Only the document URLs are answered by the docs
mount; the page request reaches the UI mount. A MountCustomizer that matches every mount and ends
every request stops this fall-through (see Mount Customizers).
The page is application code. It never bypasses a document’s access policy: a page that shows a
protected document must send the reader’s credentials itself. The page route has no access control
of its own: @ApiDocs guards only the document URLs, so put the page behind an access check of its
own when it must not be public.
UI asset integrity and content security policy
Section titled “UI asset integrity and content security policy”Integrity. Self-host the UI’s assets, or pin each external script by its exact version, with
integrity and crossorigin, as the example’s page does:
<redoc spec-url="/apidocs/public/openapi.json"></redoc><script src="https://cdn.jsdelivr.net/npm/[email protected]/bundles/redoc.standalone.js" integrity="sha384-w447zOpYfw/1Tv/5AK9NfHTlQIqE3RVR6KY62jCyy9zNDgO64cMwGGP1Fj0zJVf5" crossorigin="anonymous"></script>Policy. Start from the restrictive base policy
default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'
and add the pinned script’s exact URL, path and file name included, to script-src. Never add the
CDN origin, a path prefix, or a wildcard. The additions each UI needs were measured in a browser,
rendering the example’s public document:
| UI | Additions beyond the base policy and the script URL | Why |
|---|---|---|
| Redoc 2.5.4 standalone | style-src 'unsafe-inline'; worker-src blob: |
Redoc injects its styles at run time, which no hash or nonce can cover, and fails to render without them; its search runs in a worker created from a blob: URL |
Scalar 1.72.4 standalone (@scalar/api-reference) |
style-src 'unsafe-inline' |
Scalar injects its styles at run time and renders unstyled without them |
Everything else stays blocked, by decision: Redoc’s data: section-link icon and its footer logo
from another origin, and Scalar’s web fonts from another origin (system fonts are used instead).
Neither UI’s rendering of operations, schemas, or search depends on them. Scalar features that
contact other services were not exercised and stay blocked by connect-src 'self'. When you change
a pinned version, recompute the script’s integrity hash from the new file (a stale hash makes the
browser refuse the script, so the page fails closed) and re-measure the policy. The example’s
policy:
static final String CONTENT_SECURITY_POLICY = "default-src 'none'; script-src 'self' " + REDOC_SCRIPT_URL + "; style-src 'self' 'unsafe-inline'; connect-src 'self'; worker-src blob:; frame-ancestors 'none'";Do not loosen the policy further. A page that reads a protected document must not keep bearer
tokens in localStorage.
Document access and middleware
Section titled “Document access and middleware”The docs mount runs in the SYSTEM_FIRST phase, so document requests bypass controls attached to
the mount a document describes:
- Not run for document routes. API-scoped middleware,
RouterLifecycleHooks,RequestInterceptors that overridebeforeRequest, andMountCustomizers whosematchesrejects the docs mount. - Run for document routes. ROOT-scoped middleware and
BEFORE_MOUNTSrouter customizers on the main router, and everyMountCustomizerwhosematchesaccepts the docs mount. A customizer that adds a catch-all handler to every mount stops fall-through under the prefix (see Mount Customizers). - Public documents. No
OperationHandlerContributorruns for their routes. - Startup warning. One WARN per document names each uncovered mount-scoped control (see Controls that skip the document routes).
An IP allowlist that guards /api/mgmt as API-scoped middleware therefore does not guard
/apidocs/management/openapi.json. Move such a rule to the main router and match the document URLs
too.
A protected document’s 401 and 403 are problem responses its route writes itself; the
application’s error pipeline, exception mappers, and error interceptors never see them (see
Answers). Each protected read or denial completes as a RestRequestCompletedEvent whose
operation() is the document’s synthetic descriptor, carrying the documented application’s name,
and emits the same authorization-decision and credential-rejection events as an equally annotated
resource request. A public document read is claimed by no operation and completes as an
HttpRequestCompletedEvent (see Events and metrics).
What redaction keeps out of a document
Section titled “What redaction keeps out of a document”A generated document never publishes:
- Reserved names a request body refuses but does not publish, such as a
@JsonIgnoremember beside@JsonAnySetterextras (see Reserved-name redaction). ApropertyNamesthat a JSON mapper profile’s own schema override declares is not a recorded refusal and stays in the document, so a hidden property it names is your own disclosure. - Hidden inputs, in any position (see Hidden inputs). Hiding a composite
parameter with
@Parameter(hidden = true)or@Schema(hidden = true), or its type with@Hidden, hides all its fields. - Hidden operations (see Hidden operations), nor roles, actions, or other checks that OpenAPI cannot express (see What is never published).
A member or type that a generator still describes in a request body or a response although it
carries @Hidden or @Schema(hidden = true) fails publication instead of leaking. The message names
the fix that works at that position: @Schema(hidden = true) on the property’s own field or getter;
on the members that reference a hidden type; or, for an enum constant, @JsonUnwrapped content, or a
property bound through a setter, builder, or creator parameter, removing it from the published type
or hiding the operation (see Hidden members of a request body
and Refusals of output types). Know the limits of that check:
- It inspects the bound body type. A custom or decorating
OperationSchemaSourcethat returns another type’s description can publish that type’s@Hiddenmembers. - On output, it covers the members the output generator describes.
hiddendeclared on a response’s or a parameter’s@Header,@Schema,@Content(schema = …), or@ArraySchemais warned, not honored: the referenced schema is published, in public documents too (see Response content and headers and Ignored schema members).
Validation strategy and what a document claims
Section titled “Validation strategy and what a document claims”What a generated document’s input schemas claim depends on the mount’s request-validation strategy:
| Strategy | The input schemas are | enforcement |
|---|---|---|
web-validation |
The schemas the gate enforces, minus redacted reserved names | active |
none |
Descriptive | disabled |
| A custom strategy | The schemas it received, descriptive | unknown |
openapi-contract |
An application with its own contract serves that contract; one that validates against the shared global contract fails startup with an enabled document | — |
A public document omits these internals and records only the pattern dialect; a protected one
records the strategy, the schema source, and the enforcement (see
Validation disclosure). Under any strategy, an application with its own
contract location serves that contract (see
Serving an application’s own contract), and the shared
global contract is served only by the application itself (see
Serving the shared openapi-contract file).
Serving an application’s own contract
Section titled “Serving an application’s own contract”- Write the contract as OpenAPI 3.0 or 3.1: its
openapiis3.0.<n>or3.1.<n>, and OpenAPI 3.2 fails startup. Its file name ends in.json,.yaml, or.ymland chooses the parser. - Point the application at it with
@RestApplication.openapiPathorjaxrs.applications.<name>.openapiPath; configuration wins. Each application needs a location of its own: two applications sharing one file after path normalization fail startup. - Keep
@ApiDocs, and configure noinfoorserverUrland no@OpenAPIDefinition: the contract is served as parsed, in JSON and YAML, and never rewritten. - Describe exactly the routed operations. Every Operation Object needs an
operationIdthat no other one repeats. Every visible routed operation needs a route operation with itsoperationId, HTTP method, and path; hidden operations may be described or left out. Webhook and callback operations describe outbound calls: they are exempt from the unrouted-id check and never matched, but each needs anoperationIdthat no route uses. Every otheroperationIdmember, such as one in a Link, must name a routed operation, is never matched or counted, and, in an Operation or Link Object, must not describe that route’s hidden inputs (see Operation ids). - Keep it self-contained and safe. Only local references, no described hidden parameter or form field, and plain object schemas for form bodies.
Startup refuses every violation, listing them all (see
Startup checks of a served contract). It warns when
servers[0].url is not the mount path (see
Warnings of a served contract). Under openapi-contract a malformed
servers URL fails startup (see Contract and runtime). A relative location resolves to a
working-directory file before a classpath resource of the same name, and that file then shadows the
packaged contract, with a WARN (see
Where the contract is read from).
Know the residuals. Nothing compares the contract’s schemas with what web-validation enforces, so
drift between them goes undetected; keep them in step (see
Contract and runtime). A served contract’s form-body schemas may declare
keywords the runtime does not enforce, dependencies included, and a developer-authored contract
can still name a hidden input in the positions under Not checked: a served contract
is not fully checked for hidden inputs.
Serving the shared openapi-contract file
Section titled “Serving the shared openapi-contract file”Under openapi-contract, an application that validates against the shared global
jaxrs.openapiPath cannot enable a document: startup fails, because that file describes every
mount, public and management alike (see Startup Checks). Either give each
application its own contract location (see
Serving an application’s own contract), or serve the shared
file yourself, with a RouterMount like the UI mount of
Serving a UI under the prefix that returns the contract resource.
That route is yours: @ApiDocs does not guard it, so put it behind an access check at least as
strict as the most restricted mount the file describes.
Pattern dialect and the validation input bounds
Section titled “Pattern dialect and the validation input bounds”Patterns are published verbatim and are Java regular expressions; every generated document records
"patternDialect": "java.util.regex" (see Patterns). Application-authored patterns
appear exactly as written, such as the example’s ItemUpdate record:
public record ItemUpdate( @Pattern(regexp = "^[A-Za-z0-9 ]{1,64}(?![\\s\\S])") String name) {}A client that evaluates them in another dialect may read them differently. The web-validation
gate accepts a value when the pattern matches any part of it, so anchor a pattern at both ends when
the whole value must match; in java.util.regex, $ also matches before a final line terminator,
so end the pattern with (?![\s\S]) as the example does (see dev.vertique:vertique-json-schema).
Release note: the web-validation gate, with or without documents. These behaviors apply to
every application that validates with web-validation, whether or not any document is enabled:
- Input bounds. A string value or object key that reaches a pattern position (
pattern,patternProperties, or a pattern-bearingpropertyNames) or the formatidn-hostname,idn-email, orregexis rejected with400when it is longer thanjaxrs.validationPatternMaxChars(4,096 UTF-16 code units), or when those strings and keys add up to more thanjaxrs.validationPatternMaxTotalChars(262,144) in one request. Every other format is neither bounded nor counted, so pattern-free bodies of dates, timestamps, or identifiers are unaffected. Raise either limit in configuration; the total may not be smaller than the per-string limit (seedev.vertique:vertique-rest-core). - Linear format checks.
uri,uri-reference,url,json-pointer,relative-json-pointer,json-pointer-uri-fragment, anduri-templateare decided by linear implementations instead of the validation engine’s regular expressions, so values at the edges, such as unencoded spaces, may be judged differently, and values long enough to overflow the engine’s checks are judged instead of failing with an error.urlkeeps the engine’s scheme and host filtering and also rejects a dotted-quad label with a leading zero, such ashttp://8.08.8.8/. The “WebValidationStrategy” section ofdev.vertique:vertique-rest-validationlists the verdict differences. - Case-insensitive body types. A key of a case-insensitively bound type counts toward the total
up to three times per validation of its object, and once per
anyOfbranch for a polymorphic type; nesting does not multiply the count.
Output renames and documentation warnings
Section titled “Output renames and documentation warnings”- Renamed output properties. A response member that
@Schema(name = ...)describes under a name other than the one it is serialized under fails startup, because the document would describe a property the response never carries. Rename it on the wire with@JsonProperty, or remove the@Schema(name)(see Refusals of output types). - Documentation the document does not publish is warned at startup, once the document is built:
a
@Parameter(required = true)whose requiredness the runtime cannot determine, non-documentation@Schemaand@ArraySchemamembers on an input, response attributes the document omits, andinfoextensions not namedx-(see Metadata warnings). The examples start with none of these warnings: keep annotations to what the runtime does.
Differences from the Maven plugin’s document
Section titled “Differences from the Maven plugin’s document”A runtime document and a document the Swagger Maven plugin writes at build time are independent producers, and no parity between them is promised. Differences come from which authority decides each part:
- Schemas. The plugin’s model converters and Swagger core model describe types; the runtime publishes the schemas the framework’s input generator built for the validation gate and the schemas its output generator builds for responses, under the operation’s JSON mapper profile.
- Parameters and request bodies. The runtime writes
requiredfrom what the binding and the validation gate enforce; documentation annotations cannot add it. - Responses. The runtime infers responses from return types, and a declared success status
without content keeps the inferred schema, where swagger-core publishes none unless
useReturnTypeSchemaistrue(see A declared success status without content). - Security schemes. The runtime describes each scheme from its
SecuritySchemeHandler. - Configuration.
infoandserversmay come fromapidocs.documents.<name>configuration. - Disclosure. The runtime removes reserved names and hidden inputs and adds
x-vertique-validation. - Hiding. The runtime resolves a composed
@Hiddenone meta level deep, which swagger-core’s reader does not (see Hidden operations). A plugin configuration that lists implementation classes, rather than discovering resource packages, also keeps operations hidden by@Hiddenon an interface or interface method, which the runtime leaves out.
Compared on examples/vertique-example-services-codegen/ for property names, types, requiredness,
composition, readOnly, writeOnly, and recursion, the two documents agree, and its recorded list
of expected differences, examples/vertique-example-services-codegen/src/test/resources/apidocs/maven-comparison-expected-differences.json,
is empty. Outside those dimensions they differ: component names (ChargeRequest against
charge.request), openapi (3.0.1 against 3.1.1), a root $schema in the runtime
components, format members such as int64 in the Maven document, and requestBody.required: true
in the runtime document only.
Dependencies
Section titled “Dependencies”| Module | Why |
|---|---|
dev.vertique:vertique-rest-jaxrs |
The declared-application view, the operation publication seam the module consumes, and the ApiDocsModuleInstalled marker |
dev.vertique:vertique-rest-core |
RouterMount, MountMeta, the extension phases, JaxRsConfig default headers, ResponseProducerBinding, SecuritySchemeHandler and SecuritySchemeDescription (rendered as securitySchemes), and RestConfigurationException |
dev.vertique:vertique-security-core |
AccessPolicy, the type of @ApiDocs.policy, and the resolver that validates it and reads its direct requirements |
dev.vertique:vertique-core |
ConfigParser, configuration path navigation, ConfigurationException, @KeyedBy, and JsonMapperProfileRegistry |
dev.vertique:vertique-json-schema |
The redaction manifest each published request body is verified against and redacted by, and whose digest is part of the comparison between instances; the input generator that reports hidden members of a request body; the output generator that describes response types and reports their renamed and hidden members |
io.vertx:vertx-json-schema |
Compile dependency; evaluates whether a captured request-body schema accepts an absent body, which decides the request body’s required |
io.swagger.core.v3:swagger-annotations-jakarta |
@OpenAPIDefinition, read for info on the declaring interface, and the operation, parameter, request-body, response, tag, example, and hiding annotations described in Swagger annotations and Operation Responses |
Jackson (jackson-databind, jackson-dataformat-yaml) |
Writes the JSON and YAML forms; YAML brings SnakeYAML |
Dagger and jakarta.inject-api |
Module bindings |
The module depends on neither io.vertx:vertx-openapi nor io.vertx:vertx-web-openapi-router and
adds no route beyond the document URLs. It is not part of any starter.
Verification
Section titled “Verification”- Start a component that lists
OpenApiDocsModulewith an application whose@ApiDocspolicy is one@PermitAlland theinfoconfiguration, thenGET <prefix>/<name>/openapi.jsonand.yaml: expect200, the exact content type, anETag, andCache-Control. - Repeat with
If-None-Matchset to the returnedETag, toW/plus the tag, and to*: expect304with no body. HEADthe same URLs: expect the200headers with no body.- Request
<prefix>/unknown/openapi.json, a trailing-slash variant of a public document, and aPOST: expect the response of the later mounts, never one from this mount. - Annotate an application
@ApiDocs(policy = <a policy with @RolesAllowed("docs-reader")>, securityScheme = "bearerAuth")with the JWT handler configured asbearerAuth: expect401without a token and403with a token lacking the role, each as a problem response withCache-Control: no-storeand noETag; with the role, expect200withCache-Control: private, no-storeandVary: Authorization, and304for a matchingIf-None-Match. With the role, a trailing-slash variant answers404. - With that protected document, observe one
RestRequestCompletedEventper read or denial whose operation id isapidocs:<name>:jsonorapidocs:<name>:yaml. - Declare a policy that is one
@DenyAllwithsecurityScheme = "bearerAuth": expect401without a token and403with any valid token, never200or304, and no document bytes. - Run an interface compiled against the old
@ApiDocson this version: expect startup to fail naming the application and its interface and saying to recompile. - Name a
securitySchemeno handler registers: expect startup to fail startingApplication '<name>' (declared by <interface>)and the server not to listen. - Set
apidocs.documents.<name>.enabledtofalse: expect no document route and no publication. - Key an entry by an undeclared name, add
accessto an entry, or setserverUrltoftp://host: expect startup to fail naming the configuration path. - Declare a
GET /{path: .*}route on a mount at/: expect startup to fail naming the route and the document URL. - Mark a query parameter
@Parameter(hidden = true): expect its name in neither form of the document, while a request still binds and validates it. - Mark a path parameter
@Parameter(hidden = true): expect startup to fail naming the operation and the parameter. - Give a body type a
@JsonAnySetterand a@JsonIgnoremember, with the request-validation gate installed: expect the ignored name in neither form of the document, while the gate still refuses a body that holds it. - Bind an
OperationSchemaSourcethat edits the generated body schema: expect startup to fail naming the operation and the source class. - Annotate an operation with
@Operation(summary = ...)and its class with@Tag(name = ...): expect the summary and the tag on the operation, and the tag in the roottags. - Mark an operation
@Hidden: expect it, its components, and the tags only it declared in neither form of the document, while a request to its route is still answered. - Put
@Parameter(required = true)on a query parameter with@DefaultValue: expect startup to fail naming the operation and@Parameter.requiredon that parameter. - Put
@Parameter(schema = @Schema(maxLength = 5))on a query parameter: expect its captured schema published unchanged and oneDocumentWarningswarning naming@Parameter.schema.maxLength. - Return
Future<Item>from a@Produces(APPLICATION_JSON)method without@ApiResponse: expect exactly one response,200with descriptionOK, whoseapplication/jsonschema references<operationId>.response, a component holdingItem’s serialized property names; returnvoidand expect204; returnResponseand expect onlydefault. - Add
@ApiResponse(responseCode = "200", description = "The item")and@ApiResponse(responseCode = "404", description = "Missing"): expect200with that description and the inferred content, and404without content. - Put
@Hiddenalone on a field of the returned type: expect startup to fail naming the operation, the member,@Hidden, and the fix; move to@Schema(hidden = true)on that field and expect the property absent from the component and startup to succeed. - Put
@Schema(name = "remark")on a returned field serialized asnote: expect startup to fail naming the member,note, andremark; setapidocs.documents.<name>.enabled: falseand expect startup to succeed. - Add
linksto a declared@ApiResponse: expect it in neither form of the document and oneDocumentWarningswarning naming@ApiResponse.links on status <status>. - Put
@SecurityRequirement(name = "bearerAuth")and@RolesAllowed(...)on an operation of a public document, with the JWT handler configured asbearerAuth: expect the operation’s last member"security":[{"bearerAuth":[]}],components.securitySchemes.bearerAuthas{"type":"http","scheme":"bearer","bearerFormat":"JWT"}, the role in neither form, and oneDocumentWarningswarning listing the operation. - Require a scheme whose handler returns no
openApiDescription(): expect startup to fail naming the operation and the scheme; setapidocs.documents.<name>.enabled: falseand expect startup to succeed with the route still refusing unauthenticated requests. - Give an application with a public
@ApiDocspolicy@RestApplication(openapiPath = "openapi/partner.yaml")with that resource on the classpath: expect both forms to parse to a tree equal to the contract, the stored line to namesource: served contract, and the source INFO line to name the resource URL. - Place a file of the same relative name in the working directory: expect it served instead, and the shadowing WARN.
- Remove a routed operation from the contract, add an
operationIdthe mount does not route, or add a$refto another file: expect startup to fail withThe served contract of application '<name>' is refused:naming each violation by operation id or JSON Pointer. - Configure
apidocs.documents.<name>.infofor that application: expect startup to fail naming the configuration path. - Point two documented applications at
a.yamland./a.yaml: expect startup to fail naming both applications. - Give the contract no
servers: expect it served withoutserversand oneserversWARN.