OpenAPI and Swagger UI
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Quarkus generates an OpenAPI document for a REST application largely for free: the smallrye-openapi extension
inspects every Quarkus REST resource at build time and turns it into a
MicroProfile OpenAPI model, no annotations required to get started. This page covers what gets generated
automatically, how to enrich that generated model with annotations or a hand-maintained document fragment, the
/q/openapi and /q/swagger-ui endpoints Quarkus exposes for it, and how the resulting code-first flow compares
to a contract-first alternative.
SmallRye OpenAPI basics
Installing the extension
./mvnw quarkus:add-extension -Dextensions=smallrye-openapi
This pulls in SmallRye OpenAPI, the
MicroProfile OpenAPI implementation Quarkus uses.
If the quarkus-rest (Quarkus REST) or quarkus-rest-jackson extension is already present, no further wiring is
needed — SmallRye OpenAPI scans the same JAX-RS-annotated resource classes at build time.
What is generated with zero annotations
Given a plain Quarkus REST resource with no OpenAPI-specific annotations at all:
package com.example.orders;
import java.util.UUID;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path("/orders")
public class OrderResource {
@GET
@Path("/{orderId}")
@Produces(MediaType.APPLICATION_JSON)
public Response getOrder(@PathParam("orderId") UUID orderId) {
return Response.ok(new Order(orderId, "CREATED", 42.50)).build();
}
}
SmallRye OpenAPI’s build-time scan already infers, from the JAX-RS annotations and the Java signature alone:
-
The path (
/orders/{orderId}) and HTTP method (GET). -
The path parameter’s name and type (
orderId, astringwithformat: uuid, from theUUIDJava type). -
The response’s content type (
application/json, from@Produces) and its schema, reflected from theOrderclass’s fields.
No @Operation, @Schema, or @APIResponse is required for the document to exist and be broadly correct — annotations only enrich what reflection already produced, they do not replace it.
Enriching the generated model with annotations
package com.example.orders;
import java.util.UUID;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.ExampleObject;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
@Path("/orders")
@Tag(name = "Orders", description = "Read and manage customer orders")
public class OrderResource {
@GET
@Path("/{orderId}")
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Fetch an order", description = "Returns a single order by its identifier")
@APIResponses({
@APIResponse(responseCode = "200", description = "The requested order",
content = @Content(mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = Order.class),
examples = @ExampleObject(name = "example",
value = "{\"id\":\"6f1c...\",\"status\":\"CREATED\",\"totalAmount\":42.50}"))),
@APIResponse(responseCode = "404", description = "Order not found")
})
public Response getOrder(@PathParam("orderId") UUID orderId) {
return Response.ok(new Order(orderId, "CREATED", 42.50)).build();
}
}
-
@Taggroups endpoints in Swagger UI’s sidebar — one tag per resource class, or per logical area, keeps large APIs navigable. -
@Operationsets the human-facingsummaryanddescriptionfor one operation. -
@APIResponse/@APIResponsesdocument status codes explicitly, including ones reflection cannot infer (a404that only ever happens on an exception-mapped path, for instance), plus example payloads. -
@Schema, placed on a DTO class or one of its fields, refines the reflected schema — adescription, aformat, anenumof allowed values, or marking a fieldrequired:
package com.example.orders;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
public class Order {
@Schema(description = "Order identifier", format = "uuid", required = true)
private java.util.UUID id;
@Schema(description = "Current order status", enumeration = {"CREATED", "PAID", "SHIPPED", "CANCELLED"})
private String status;
@Schema(description = "Total order amount, in the account's currency")
private double totalAmount;
public Order(java.util.UUID id, String status, double totalAmount) {
this.id = id;
this.status = status;
this.totalAmount = totalAmount;
}
// getters omitted for brevity
}
Merging a static document
A hand-written OpenAPI fragment placed at src/main/resources/META-INF/openapi.yaml (or openapi.json) is
merged into the reflection-generated model rather than replacing it — useful for document-level metadata that
has no natural home on a Java annotation, or for describing endpoints (a proxy, a non-JAX-RS servlet) that
SmallRye OpenAPI cannot scan:
# src/main/resources/META-INF/openapi.yaml
openapi: 3.0.3
info:
title: Orders API
version: 1.0.0
description: Read and manage customer orders
servers:
- url: https://api.example.com
description: Production
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
With a bearerAuth scheme defined this way, a protected resource references it with the standard MicroProfile
OpenAPI annotation:
package com.example.orders;
import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement;
@jakarta.ws.rs.Path("/orders")
@SecurityRequirement(name = "bearerAuth")
public class OrderResource {
// ...
}
Document-level info via configuration
Rather than a static file, info fields can also be set directly as configuration properties — convenient when
the version or description should track a build property (${quarkus.application.version}, an environment-
specific description) instead of being hand-edited in a YAML file:
quarkus.smallrye-openapi.info-title=Orders API
quarkus.smallrye-openapi.info-version=1.0.0
quarkus.smallrye-openapi.info-description=Read and manage customer orders
quarkus.smallrye-openapi.info-contact-email=api-team@example.com
quarkus.smallrye-openapi.info-license-name=Apache 2.0
These quarkus.smallrye-openapi.info-* properties are the current Quarkus-recommended way to set document
metadata from application.properties; a static openapi.yaml’s `info block and these properties can both be
present, in which case the static document is merged first and the properties override its info fields.
Exposed endpoints
The generated document: /q/openapi
The generated (and merged, and annotation-enriched) OpenAPI document is served at /q/openapi, defaulting to
YAML:
curl http://localhost:8080/q/openapi
JSON is available either via content negotiation (Accept: application/json) or the format query parameter:
curl http://localhost:8080/q/openapi?format=json
The path itself is configurable:
quarkus.smallrye-openapi.path=/q/openapi
The interactive UI: /q/swagger-ui
open http://localhost:8080/q/swagger-ui
Swagger UI renders the same document as a browsable, "try it out" console — expand an operation, fill in parameters and a request body, and fire a real request against the running application without leaving the browser.
|
Swagger UI is available by default only in dev mode and tests, exactly like the Dev UI covered on the
Developer Experience page — a production build (
|
Enabling it in production is a deliberate trade-off: it makes the API self-documenting and explorable for integrators, but also publishes the full operation list (and, if "try it out" is left enabled, a way to call every endpoint from a browser) to anyone who can reach the path — pair it with the application’s normal authentication and, if the UI itself should not be public, a path-level access restriction.
Generating a static schema file at build time
quarkus.smallrye-openapi.store-schema-directory writes the generated OpenAPI document to disk as part of the
build, with no running server needed to fetch it from /q/openapi:
quarkus.smallrye-openapi.store-schema-directory=${project.build.directory}/openapi
Running ./mvnw package then produces target/openapi/openapi.yaml (JSON is also available via
quarkus.smallrye-openapi.store-schema-file-name=openapi.json), ready to feed into external tooling that needs
the contract but not a live application:
-
An OpenAPI client generator (
openapi-generator-maven-pluginand similar), run against a teammate’s or a CI build’s output instead of a hand-maintained spec. -
Contract tests that diff the current build’s schema against a previously agreed-on baseline, failing CI the moment an endpoint’s shape changes unexpectedly.
-
Publishing the document itself as a build artifact, independent of deploying the application.
Code-first vs. contract-first
Everything on this page so far is Quarkus’s default flow, code-first: resources are annotated (or left plain),
and SmallRye OpenAPI derives the document from them at build time. The alternative, contract-first (also called
API-first or spec-driven), inverts the order — the OpenAPI document is hand-written and agreed on first, and the
server interfaces are generated from it by a build-time code generator, with a hand-written class implementing
the generated interface. Quarkus can still serve that hand-written document at /q/openapi and /q/swagger-ui
(via the static-file merge described above), but the generation direction — code to spec, versus spec to code — is the fundamental difference between the two approaches, along with what each treats as the reviewed,
version-controlled source of truth.
For a fuller treatment of the API-first philosophy — when a hand-maintained contract is worth the extra process, how it changes review and versioning, and its trade-offs against the code-first default — see API-First Development: REST and gRPC, which covers contract-first REST and gRPC tooling in depth; the discussion there applies to Quarkus the same way it does to Spring Boot, since the contract-first argument is about process and tooling, not the framework generating the runtime code.