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, a string with format: uuid, from the UUID Java type).

  • The response’s content type (application/json, from @Produces) and its schema, reflected from the Order class’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();
    }
}
  • @Tag groups endpoints in Swagger UI’s sidebar — one tag per resource class, or per logical area, keeps large APIs navigable.

  • @Operation sets the human-facing summary and description for one operation.

  • @APIResponse / @APIResponses document status codes explicitly, including ones reflection cannot infer (a 404 that 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 — a description, a format, an enum of allowed values, or marking a field required:

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 (./mvnw package with no dev-mode profile) does not serve /q/swagger-ui unless explicitly told to. To keep it available in a production build:

quarkus.swagger-ui.always-include=true

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-plugin and 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.