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>
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 forUserDtoList<UserDto>/Set<UserDto>/UserDto[]->arraywithitems: UserDtoMap<String, UserDto>->objectwithadditionalProperties: 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:
componentsfragments (merged with last-wins)- operation
securityrequirements (additive) - operation
responsesand responseheaders(additive)
Merge policy:
- contributor collisions use last-wins
- manual
@APIResponsedescription 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 required403role/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-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-Afteron429
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.