Skip to main content

API-Docs Mixin Pack

The Juneau REST server ships with a four-class api-docs mixin pack in org.apache.juneau.rest.server.docs that drives every built-in Swagger / OpenAPI / Redoc endpoint on BasicRestServlet and BasicRestResource. Each class is a single-purpose @Rest-annotated resource designed to be grafted into your servlet via @Rest(mixins=...) — pick the URLs you want, leave the ones you don't.

See the guide: Generate OpenAPI docs for your API

Migrating from a 9.4.x or earlier resource that used @Rest(apiFormat="…")? See the V10 Migration Guide for the removal table and worked conversions. Short version: stop setting apiFormat, declare the mixins you want instead.

The four mixins at a glance​

MixinDefault pathsContent negotiationWhy it exists
SwaggerMixin/apiStandard Juneau content negotiation (HTML when Accept: text/html; serializer default otherwise).Swagger v2 spec emission via the Swagger bean + SwaggerUI swap.
SwaggerUiMixin/swaggerBare browser GET (no Accept) defaults to text/html; explicit Accept is honored normally.Same Swagger v2 spec mounted at an HTML-first URL. Declares @Rest(mixins=SwaggerMixin.class) so transitive resolution brings /api along for free.
OpenApiMixin/openapi, /openapi.json, /openapi.yaml/openapi uses standard Juneau content negotiation; /openapi.json and /openapi.yaml are format-pinned (ignore Accept).OpenAPI 3.1 spec emission via the OpenApi bean + RedocUI swap.
RedocMixin/redocBare browser GET (no Accept) defaults to text/html; explicit Accept is honored normally.Same OpenAPI 3.1 spec mounted at an HTML-first URL. Declares @Rest(mixins=OpenApiMixin.class) so transitive resolution brings /openapi/* along for free.

The split is intentional: spec mixins (SwaggerMixin, OpenApiMixin) emit the document and let the standard content negotiation pipeline decide the wire format. UI mixins (SwaggerUiMixin, RedocMixin) tweak content negotiation so a bare browser GET — which sends Accept: */* or no Accept at all — renders the HTML view by default, while machine clients that send Accept: application/json (or application/yaml) still get the spec in their requested format. This keeps the same /swagger URL useful for both "open in browser" and "fetch as JSON" without query-string toggles.

Default mount on BasicRestServlet / BasicRestResource​

Any subclass of BasicRestServlet or BasicRestResource gets the full pack by default. Both classes declare:

@Rest(mixins = {SwaggerUiMixin.class, RedocMixin.class})
public abstract class BasicRestServlet extends RestServlet implements ... { ... }

Because SwaggerUiMixin transitively pulls in SwaggerMixin and RedocMixin transitively pulls in OpenApiMixin, the mixin walk dedupes by class identity and the resulting servlet serves six URLs:

URLMixinWire format
GET /apiSwaggerMixinSwagger v2 spec; content-negotiated.
GET /swaggerSwaggerUiMixinSwagger UI (HTML default; JSON / XML on explicit Accept).
GET /openapiOpenApiMixinOpenAPI 3.1 spec; content-negotiated.
GET /openapi.jsonOpenApiMixinOpenAPI 3.1 spec; always JSON.
GET /openapi.yamlOpenApiMixinOpenAPI 3.1 spec; always YAML.
GET /redocRedocMixinRedoc UI (HTML default; JSON / XML on explicit Accept).

Subclasses that just want this surface need to declare nothing — extending BasicRestServlet is enough.

Composing your own surface​

The mixins are independent — pick the URLs you want. The four common compositions:

Just the OpenAPI spec (no Swagger v2, no UI)​

@Rest(path = "/api", mixins = OpenApiMixin.class)
public class ApiResource extends RestServlet {
@RestGet("/items") public List<Item> items() { ... }
}
// Serves /api/items, /api/openapi, /api/openapi.json, /api/openapi.yaml.

OpenAPI spec plus the Redoc UI​

@Rest(path = "/api", mixins = RedocMixin.class)   // pulls OpenApiMixin in transitively
public class ApiResource extends RestServlet {
@RestGet("/items") public List<Item> items() { ... }
}
// Serves /api/items, /api/openapi, /api/openapi.json, /api/openapi.yaml, /api/redoc.

Full surface (mirrors BasicRestServlet, but on a plain RestServlet)​

@Rest(path = "/api", mixins = {SwaggerUiMixin.class, RedocMixin.class})
public class ApiResource extends RestServlet {
@RestGet("/items") public List<Item> items() { ... }
}
// Serves /api/items, /api/api, /api/swagger, /api/openapi, /api/openapi.json,
// /api/openapi.yaml, /api/redoc.

Drop one half of the inherited pack​

BasicRestServlet already mixes in both UI halves. To keep only the OpenAPI side on a subclass, opt out of inherited mixins and re-declare the mixin you want:

@Rest(noInherit = {"mixins"}, mixins = RedocMixin.class)
public class MyApi extends BasicRestServlet {
@RestGet("/items") public List<Item> items() { ... }
}
// Serves /items, /openapi, /openapi.json, /openapi.yaml, /redoc — but NOT /api or /swagger.

Standalone mount via @Rest(paths=...)​

Each mixin is itself a fully-fledged @Rest-annotated resource and can be subclassed and mounted as its own top-level servlet:

@Rest(paths = {"/openapi", "/openapi.json", "/openapi.yaml"})
public class OpenApiDocs extends OpenApiMixin { }

Both deployment styles (mixin into an existing servlet vs. mount as a sibling servlet) work the same way under Spring Boot and under the Jetty microservice — see Spring Boot vs. microservice below.

No-Accept-defaults-to-HTML on UI mixins​

The two UI mixins both declare @Rest(defaultAccept = "text/html"):

@Rest(paths = {"/swagger"}, mixins = SwaggerMixin.class, defaultAccept = "text/html")
public class SwaggerUiMixin implements BasicUniversalConfig { ... }

@Rest(paths = {"/redoc"}, mixins = OpenApiMixin.class, defaultAccept = "text/html")
public class RedocMixin implements BasicUniversalConfig { ... }

defaultAccept only fires when the request has no Accept header (or sends */*, which collapses to "no preference"). When that happens, the framework injects Accept: text/html before content negotiation runs, and the SwaggerUI / RedocUI swap fires to render the HTML view.

That gives the natural behaviors:

RequestResult on /swagger and /redoc
Bare browser GET (no Accept)HTML view via the UI swap.
Accept: application/jsonSpec serialized as JSON via the standard serializer.
Accept: application/xmlSpec serialized as XML.
Accept: */*HTML view (same as bare browser).

The spec mixins (/api, /openapi) do not declare defaultAccept, so the standard content-negotiation default (the serializer's preferred media type, typically JSON) wins when no Accept header is sent.

Format-pinned /openapi.json and /openapi.yaml​

OpenApiMixin declares three @RestGet methods, one per mount path:

@RestGet("/*")                                              // content-negotiated
public OpenApi getOpenApi(RestRequest req) { ... }

@RestGet(path = "/openapi.json", produces = "application/json")
public void getOpenApiJson(RestRequest req, RestResponse res) throws Exception {
res.getDirectWriter("application/json").write(req.getOpenApi().orElseThrow(NotFound::new).toJson());
}

@RestGet(path = "/openapi.yaml", produces = "application/yaml")
public void getOpenApiYaml(RestRequest req, RestResponse res) throws Exception {
res.getDirectWriter("application/yaml").write(req.getOpenApi().orElseThrow(NotFound::new).toYaml());
}

The two .json and .yaml endpoints use RestResponse.getDirectWriter(...) to write the spec directly to the response body in the requested wire format, bypassing the standard content-negotiation pipeline. That means:

  • GET /openapi.json returns JSON regardless of Accept. Even Accept: text/html returns JSON. The path implies the format.
  • GET /openapi.yaml returns YAML regardless of Accept.
  • GET /openapi (no suffix) runs the normal pipeline — Accept: application/json gets JSON, Accept: application/yaml gets YAML, Accept: text/html gets the Redoc UI swap (via the RedocMixin mixin if it's in the pack; otherwise the serializer default).

This split lets API docs consumers use direct, unambiguous URLs for the spec (/openapi.json, /openapi.yaml) without trusting the server's content negotiation, while keeping /openapi useful for browsers.

Spring Boot vs. microservice​

The mixin pack works identically under both deployment paths because mixin instances are resolved through the importing servlet's BeanStore:

  • Microservice path. BasicBeanStore looks up each mixin class via getBean(<MixinClass>.class) first; if no bean is registered, the framework reflects a no-arg constructor on the mixin. Verified end-to-end by BasicApiDocs_JettyMicroservice_Test in juneau-integration-tests, which boots a real Jetty Microservice on an ephemeral port and hits all six URLs over HTTP.
  • Spring Boot path. SpringBeanStore extends BasicBeanStore and delegates unresolved lookups to ApplicationContext.getBeanProvider(beanType).getIfAvailable(). Same fallback to no-arg constructor if no Spring bean is registered. Verified end-to-end by BasicApiDocs_Springboot_Test in juneau-integration-tests, which boots a full Spring Boot context with embedded Tomcat and hits the same six URLs.

Both tests share a fixture (BasicApiDocsTestFixtures) so the response-shape assertions are identical across deployment paths. Drift in either direction fails CI.

Multiple OpenApiProvider beans in Spring Boot​

If your Spring Boot app registers more than one OpenApiProvider @Bean (rare, but possible when you want a default and a custom provider co-existing), mark exactly one of them @Primary:

@Configuration
public class OpenApiConfig {

@Bean
@Primary
public OpenApiProvider productionProvider() {
return new MyCustomOpenApiProvider(...);
}

@Bean
public OpenApiProvider fallbackProvider() {
return new BasicOpenApiProvider(...);
}
}

SpringBeanStore.getBean(OpenApiProvider.class) delegates to ApplicationContext.getBeanProvider(OpenApiProvider.class).getIfAvailable(), which honors @Primary correctly. The OpenApiMixin mixin sees the @Primary bean.

If you register two OpenApiProvider beans without marking one @Primary, Spring fails the context-load with BeanDefinitionOverrideException (when two @Bean methods declare the same bean id) or falls through to getIfUnique() returning null (when two @Bean methods declare different bean ids and neither is primary). Both failure modes happen before Juneau gets a chance to look the provider up, so Juneau cannot give a friendlier error here. This is a documented "known rough edge"; the fix is always to mark one bean @Primary. Both the success and the collision cases are pinned by BasicApiDocs_Springboot_MultiOpenApiProvider_Test.

The same precedence rule applies to SwaggerProvider for the Swagger v2 side: multiple beans require exactly one @Primary marking.

Migrating from apiFormat​

The 9.4.x @Rest(apiFormat = "swagger" | "openapi" | "both") annotation member, the juneau.rest.apiFormat system property, the RestContext.getApiFormat() accessor, and the ?Swagger / ?OpenApi query mirrors on group resources are all removed in 10.0.0. Compose the mixins you want instead. The V10 Migration Guide has the full removal table; the short version:

Old (9.4.x)New (10.0.0)
@Rest(apiFormat = "swagger")Default behavior of BasicRestServlet — no change needed.
@Rest(apiFormat = "openapi")@Rest(noInherit = {"mixins"}, mixins = RedocMixin.class) on a BasicRestServlet subclass.
@Rest(apiFormat = "both")Default behavior of BasicRestServlet — both halves are always mounted.
GET /api?Swagger=…GET /api (or GET /swagger for the HTML view).
GET /?OpenApi=…GET /openapi (or GET /redoc for the HTML view).
juneau.rest.apiFormat system propertyRemoved. Pick mixins at compile time.

See also​

  • Generate OpenAPI docs for your API: a guided walkthrough.
  • REST Server — Mixins and Multi-Mount Paths — the @Rest(mixins=...) and @Rest(paths=...) primitives this pack builds on, including the parent-chain mixin aggregation rules that govern how mixins compose across an inheritance hierarchy.
  • Mixin Sub-Contexts — per-mixin RestContext inheritance model; each api-docs mixin has its own sub-context so the RestResponse, defaultAccept, and produces settings only affect that mixin's endpoints.
  • Basic REST Servlet Swagger — pre-mixin background on Swagger / OpenAPI emission and components.schemas reuse.
  • V10 Migration Guide — full breaking-change table and worked migration steps for 9.4.x → 10.0.0.