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 settingapiFormat, declare the mixins you want instead.
The four mixins at a glance
| Mixin | Default paths | Content negotiation | Why it exists |
|---|---|---|---|
SwaggerMixin | /api | Standard Juneau content negotiation (HTML when Accept: text/html; serializer default otherwise). | Swagger v2 spec emission via the Swagger bean + SwaggerUI swap. |
SwaggerUiMixin | /swagger | Bare 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 | /redoc | Bare 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:
| URL | Mixin | Wire format |
|---|---|---|
GET /api | SwaggerMixin | Swagger v2 spec; content-negotiated. |
GET /swagger | SwaggerUiMixin | Swagger UI (HTML default; JSON / XML on explicit Accept). |
GET /openapi | OpenApiMixin | OpenAPI 3.1 spec; content-negotiated. |
GET /openapi.json | OpenApiMixin | OpenAPI 3.1 spec; always JSON. |
GET /openapi.yaml | OpenApiMixin | OpenAPI 3.1 spec; always YAML. |
GET /redoc | RedocMixin | Redoc 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:
| Request | Result on /swagger and /redoc |
|---|---|
Bare browser GET (no Accept) | HTML view via the UI swap. |
Accept: application/json | Spec serialized as JSON via the standard serializer. |
Accept: application/xml | Spec 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.jsonreturns JSON regardless ofAccept. EvenAccept: text/htmlreturns JSON. The path implies the format.GET /openapi.yamlreturns YAML regardless ofAccept.GET /openapi(no suffix) runs the normal pipeline —Accept: application/jsongets JSON,Accept: application/yamlgets YAML,Accept: text/htmlgets the Redoc UI swap (via theRedocMixinmixin 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.
BasicBeanStorelooks up each mixin class viagetBean(<MixinClass>.class)first; if no bean is registered, the framework reflects a no-arg constructor on the mixin. Verified end-to-end byBasicApiDocs_JettyMicroservice_Testinjuneau-integration-tests, which boots a real JettyMicroserviceon an ephemeral port and hits all six URLs over HTTP. - Spring Boot path.
SpringBeanStoreextendsBasicBeanStoreand delegates unresolved lookups toApplicationContext.getBeanProvider(beanType).getIfAvailable(). Same fallback to no-arg constructor if no Spring bean is registered. Verified end-to-end byBasicApiDocs_Springboot_Testinjuneau-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 property | Removed. 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
RestContextinheritance model; each api-docs mixin has its own sub-context so theRestResponse,defaultAccept, andproducessettings only affect that mixin's endpoints. - Basic REST Servlet Swagger — pre-mixin background
on Swagger / OpenAPI emission and
components.schemasreuse. - V10 Migration Guide — full breaking-change table and worked migration steps for 9.4.x → 10.0.0.