Files
Flash5/flash-extensions/flash-ext-openapi/README.md
T
Zakaria El OrcheandClaude Opus 5 2fbe65fcc5 feat(ext-openapi): document what the code already says
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>
2026-09-23 13:31:28 +00:00

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 handle returns — an object, a List<T>, a Map<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 BodyHandlerJsonHandler 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.schema omitted 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 = true wraps whichever schema was chosen.
  • contentType = NONE documents 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:

  • components fragments (merged last-wins)
  • operation security requirements (additive)
  • operation responses and response headers (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.