Files
Flash5/flash-extensions/flash-ext-openapi
Zakaria El OrcheandClaude Opus 5 f68e661296 feat(ext-validation): add request validation with compiled constraints
Standard jakarta.validation annotations, compiled once per type into a flat
check table. No configuration: constraints come from the annotations already on
your types, and ValidationException extends HttpException with status 422 so
the default handler renders it without this extension registering anything.

    record CreateUser(@NotBlank @Size(max = 80) String name,
                      @Email String email,
                      @Min(18) int age) {}

    CreateUser dto = validation.body(req, CreateUser.class);

Annotations only — Hibernate Validator's engine is deliberately absent. It
resolves constraints reflectively per call and pulls ~2 MB plus EL, which is
the per-request cost this module exists to avoid. jakarta.validation-api is
~90 KB of annotations.

The passing path allocates nothing. Constraints resolve at first use into an
opcode plus operands cached in a ClassValue, so there is no map lookup and no
lock. Fields are read through MethodHandles adapted to an exact signature —
(Object)Object for references, (Object)long for primitive integrals — so
invokeExact neither boxes nor builds the argument array Field.get and
Method.invoke allocate. Checks are a flat array walked by a tableswitch rather
than a class hierarchy behind a virtual call. @Size reads a length the object
already knows and @Email scans with indexOf, because Pattern.matcher allocates
a matcher and two int arrays per call. Messages are pre-rendered at compile
time. The violation list and the exception exist only once something fails.

@Pattern is the marked exception: its regex compiles once but matcher()
allocates per call.

Constraints are read from declared fields, so records and plain classes take
one code path — a constraint on a record component propagates to its backing
field.

Jakarta null semantics are exact: only @NotNull rejects null.

flash-ext-openapi now mirrors the same annotations into the generated schema —
minLength, maxLength, minItems, minimum, maximum, pattern, format: email and
required — via an optional jakarta.validation dependency detected at boot. A
type declares its rules once and both the validator and the published contract
read them. An explicit @Schema still wins; the bridge only fills keys nobody
set, and without the annotations on the classpath the bridge class is never
loaded.

flash-ext-jackson is optional too: validate(value) works without it, only
body(req, type) needs a codec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 12:20:33 +00:00
..

flash-ext-openapi

OpenAPI 3.0.3 generation + Swagger UI for Flash.

What it provides

Route Description
GET /openapi.json OpenAPI spec JSON
GET /openapi.yaml OpenAPI spec YAML
GET /openapi/swagger Swagger UI

Install

FlashApp.create(8080)
    .install(new JacksonExtension())
    .install(new OpenApiExtension("/openapi", "My API", "1.0.0"))
    .scan("com.acme.handlers")
    .startAndBlock();

Operation annotation

@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"})
@APIResponse(
    responseCode = "200",
    description = "User found",
    content = @Content(contentType = ContentType.JSON, schema = UserDto.class)
)
public final class GetUser extends RequestHandler { ... }

Response patterns

Single object

@APIResponse(
    responseCode = "200",
    description = "User found",
    content = @Content(contentType = ContentType.JSON, schema = UserDto.class)
)

Array

@APIResponse(
    responseCode = "200",
    description = "Users listed",
    content = @Content(contentType = ContentType.JSON, schema = UserDto.class, array = true)
)

No content

@APIResponse(
    responseCode = "204",
    description = "Deleted",
    content = @Content(contentType = ContentType.NONE)
)

Inferred from handler return type

@APIResponse(
    responseCode = "200",
    content = @Content
)

If content.schema is omitted, schema is inferred from the handler handle(...) return type. Explicit content.schema always wins over inference.

Inference defaults:

  • UserDto -> object schema for UserDto
  • List<UserDto> / Set<UserDto> / UserDto[] -> array with items: UserDto
  • Map<String, UserDto> -> object with additionalProperties: UserDto

DTO schema metadata

@Schema(name = "User", title = "User DTO", description = "Public user", deprecated = false)
public class UserDto {

    @SchemaProperty(title = "ID", required = true, example = "USR-100", enumeration = {"USR-100", "USR-101"})
    public String id;

    @SchemaProperty(hidden = true)
    public String internalDebug;
}

Supported field-level exclusion:

  • @Schema(hidden = true) / @SchemaProperty(hidden = true)
  • @JsonIgnore
  • @JsonIgnoreProperties(...)
  • transient / static

Contributor API

OpenAPI is extension-agnostic. Other extensions contribute with OpenApiContributor via OpenApiContributorRegistry.

Supported contribution surfaces:

  • components fragments (merged with last-wins)
  • operation security requirements (additive)
  • operation responses and response headers (additive)

Merge policy:

  • contributor collisions use last-wins
  • manual @APIResponse description always wins over contributors for the same status

OIDC interop

When flash-ext-oidc is installed, OpenAPI integrates automatically:

  • security scheme under components.securitySchemes
  • per-operation security
  • auto responses (class-based handlers):
    • 401 Authentication required
    • 403 role/scope required messages when applicable

Manual @APIResponse for the same status code always wins.

Limiter interop

When flash-ext-limiter is installed, handlers with @Limit automatically get response headers documented in OpenAPI:

  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
  • Retry-After on 429

If 429 is missing, it is auto-added as Too Many Requests.

Notes

  • Operations are collected from final boot-time routes for class-based handlers with @ApiOperation.
  • Documented paths always match runtime paths (including scope namespaces/prefixes/rewrites).
  • Route path params are auto-discovered from /{id}.
  • Parameter annotations are mainly for query/header/cookie enrichment.
  • Output responses are sorted by numeric status code.