Every class-based route is documented now, annotated or not: a route without @ApiOperation used to be dropped with a warning, which made the document lie by omission. Read off the handler: the request body from the type it declares (or from the new @RequestBody, for one that reads the body itself), the success schema from the most specific handle it implements, and the media type from @Consumes. Failures are described too. Any 4xx or 5xx without an explicit schema documents the error object Flash actually answers with, written once under components.schemas.Error — before this, a declared 4xx inherited the success schema, which was simply wrong. And an answer two or more operations give identically is hoisted into components.responses and referenced, so the 401 of every guarded route appears once rather than on every path. @Content gained an example, and the schema registry moved into its own class. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.2 KiB
flash-ext-openapi
OpenAPI 3.0.3 generation and Swagger UI, built from what the handlers already say about themselves.
What it provides
| Route | Description |
|---|---|
GET /openapi.json |
OpenAPI spec JSON |
GET /openapi.yaml |
OpenAPI spec YAML |
GET /openapi/swagger |
Swagger UI |
FlashApp.create(8080)
.install(new JacksonExtension())
.install(new OpenApiExtension("/openapi", "My API", "1.0.0"))
.scan("com.acme.handlers")
.startAndBlock();
What you get without writing anything
Every class-based route is documented, annotated or not. Read off the code:
- path parameters, from
/{id}in the route - the request body, from the handler's own body type (see below)
- the response schema, from what
handlereturns — an object, aList<T>, aMap<String, T> - error responses, in the one shape Flash answers failures with:
{"error": "...", "status": 404} - security and rate limiting, from the extensions that enforce them
Annotations add what the code cannot say: prose, extra statuses, examples. They never repeat it.
Request bodies
A handler that extends BodyHandler — JsonHandler and XmlHandler, and anything else that
reads a format — declares its body type in its signature, and that is the whole documentation:
@POST("/users")
public final class CreateUser extends JsonHandler<NewUser> {
@Override protected Object handle(Request req, Response res, NewUser body) {
return users.create(body);
}
}
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/NewUser' }
The media type comes from @Consumes on the base class, so an XML handler documents itself as
XML without a word from the route.
For a handler that reads the body by hand, or to describe it as something else, declare it:
@PUT("/users")
@RequestBody(value = User.class, array = true, description = "Users to store")
public final class ReplaceUsers extends RequestHandler { ... }
Operations
@GET("/users/{id}")
@ApiOperation(summary = "Get user", description = "Returns one user", tags = {"users"})
@Parameter(name = "expand", in = ParameterIn.QUERY, type = SchemaType.STRING, examples = {"roles", "permissions"})
public final class GetUser extends RequestHandler { ... }
@ApiOperation is optional: without it the route is still in the document, with no summary.
Responses
The success response is inferred. Declare one only to say more:
@APIResponse(responseCode = "200", description = "User found",
content = @Content(schema = UserDto.class, example = "{\"id\":\"usr-1\"}"))
@APIResponse(responseCode = "409", description = "That email is taken")
@APIResponse(responseCode = "204", content = @Content(contentType = ContentType.NONE))
content.schemaomitted on a 2xx: the handler's return type.- Any 4xx or 5xx without an explicit schema: Flash's error object, referenced from
components. content.array = truewraps whichever schema was chosen.contentType = NONEdocuments a response with no body.
A response several operations share is written once. Identical answers — the 401 of every
guarded route, the 429 of every limited one — become components.responses entries referenced by
$ref, instead of being repeated on every path.
DTO schemas
@Schema(name = "User", title = "User DTO", description = "Public user")
public record UserDto(
@SchemaProperty(title = "ID", example = "USR-100") String id,
@SchemaProperty(hidden = true) String internalDebug) {}
Each type is described once under components.schemas and referenced everywhere it appears.
Field-level exclusion: @Schema(hidden = true), @SchemaProperty(hidden = true), @JsonIgnore,
@JsonIgnoreProperties, transient, static. jakarta.validation constraints (@NotNull,
@NotBlank, @NotEmpty, @Size, @Min, @Max, @Email, @Pattern) become the schema's own
bounds and required fields, so a rule is written once and documented for free.
Contributor API
OpenAPI is extension-agnostic. Other extensions contribute through OpenApiContributor, held in
OpenApiContributorRegistry:
componentsfragments (merged last-wins)- operation
securityrequirements (additive) - operation
responsesand responseheaders(additive)
Manual @APIResponse description always wins over a contributor's for the same status.
Security interop
With flash-ext-security-core installed, every registered mechanism's scheme lands under
components.securitySchemes, and every operation carrying a security annotation lists them as
security alternatives with automatic 401 and — for roles or scopes — 403 responses.
Limiter interop
When flash-ext-limiter is installed, handlers with @Limit document X-RateLimit-Limit,
X-RateLimit-Remaining, X-RateLimit-Reset, and a 429 with Retry-After.
Notes
- Operations come from the final boot-time routes, so documented paths match runtime paths, namespaces, prefixes and rewrites included.
- Lambda routes are not documented: there is no class to read.
- Responses are sorted by status code; the document is rebuilt only when a route is added.