feat: typed bodies, injected services, one Jackson module per format #20

Merged
Relism merged 3 commits from feature/handlers/typed-bodies-and-injected-services into master 2026-09-23 14:11:17 +00:00
Owner

A handler now says what it needs and what it takes, and everything downstream reads it off the class.

Core. @Inject on a field is filled inside bind, before onInit, once per handler at boot; the request path still reads a field. A static or final field is refused, and a type nothing provides fails the boot naming the field. BodyHandler<B> puts the body type in the signature and leaves reading it to the format; @Consumes says in which media type and is descriptive.

Jackson. flash-ext-jackson and flash-ext-validation become flash-ext-jackson-core, -json and -xml. Every Jackson format is the same databind model behind a different factory, so annotations, constraints and schemas are shared and only the mapper and content type differ. A typed body is always verified against its own type's jakarta constraints, whatever the format: 400 malformed, 422 constraint, neither reaching the handler. bodyFrom is gone — body has the streaming semantics, which is the path that does not allocate. JacksonExtension is JsonExtension, autoJson() is auto().

OpenAPI. Every class-based route is documented, annotated or not. The request body comes from the type the handler declares (or @RequestBody), the success schema from the most specific handle, and failures are described with the error object Flash actually answers with — a declared 4xx used to inherit the success schema. An answer several operations share is written once under components.responses.

The root POM now manages every module of this build, so a consumer imports it once and names no version again.

Green: full build, tests and coverage gates. flash-ext-jackson-core + -json + -xml are 694 main lines against the 710 the two modules they replace had, with XML gained.

🤖 Generated with Claude Code

A handler now says what it needs and what it takes, and everything downstream reads it off the class. **Core.** `@Inject` on a field is filled inside `bind`, before `onInit`, once per handler at boot; the request path still reads a field. A `static` or `final` field is refused, and a type nothing provides fails the boot naming the field. `BodyHandler<B>` puts the body type in the signature and leaves reading it to the format; `@Consumes` says in which media type and is descriptive. **Jackson.** `flash-ext-jackson` and `flash-ext-validation` become `flash-ext-jackson-core`, `-json` and `-xml`. Every Jackson format is the same databind model behind a different factory, so annotations, constraints and schemas are shared and only the mapper and content type differ. A typed body is always verified against its own type's jakarta constraints, whatever the format: 400 malformed, 422 constraint, neither reaching the handler. `bodyFrom` is gone — `body` has the streaming semantics, which is the path that does not allocate. `JacksonExtension` is `JsonExtension`, `autoJson()` is `auto()`. **OpenAPI.** Every class-based route is documented, annotated or not. The request body comes from the type the handler declares (or `@RequestBody`), the success schema from the most specific `handle`, and failures are described with the error object Flash actually answers with — a declared 4xx used to inherit the success schema. An answer several operations share is written once under `components.responses`. The root POM now manages every module of this build, so a consumer imports it once and names no version again. Green: full build, tests and coverage gates. `flash-ext-jackson-core` + `-json` + `-xml` are 694 main lines against the 710 the two modules they replace had, with XML gained. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Relism added 3 commits 2026-09-23 14:11:02 +00:00
Two things every handler was writing by hand.

@Inject on a field is filled inside bind, before onInit, once per handler at
boot: the request path still reads a field. The service is looked up by the
field's exact declared type; a static or final field is refused, and a type
nothing provides fails the boot naming the field. onInit stays for what has to
be computed, or for a service that may not be there.

BodyHandler<B> puts the body type in the signature — handle(req, res, body) —
and leaves reading it to the format. bodyTypeOf resolves that type argument
through a whole chain of bases, so tooling can read off a class what a route
takes. @Consumes says in which media type, inherited from the base class that
implements the reading, and is descriptive: the router does not enforce it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
flash-ext-jackson and flash-ext-validation become three modules:

- flash-ext-jackson-core: the Codec (one mapper, body/write/writeView),
  JacksonHandler, the outbound marshalling, and the constraint engine that
  used to be flash-ext-validation
- flash-ext-jackson-json: Json, JsonExtension, JsonHandler
- flash-ext-jackson-xml: Xml, XmlExtension, XmlHandler

Every Jackson format is the same databind model behind a different factory, so
the annotations, the constraints and the published schema are the same for all
of them: only the mapper and the content type differ, and that is all a format
module says. A route picks its format by the handler it extends — there is no
negotiation and nothing to configure.

A typed body is now always verified against its own type's jakarta
constraints, whatever the format: malformed is a 400, a broken constraint is a
422, and neither reaches the handler. The validator was already allocation-free
and stays so; validating is no longer something an application remembers to do.

bodyFrom is gone. body has the streaming semantics, because the request's
stream is reused per connection while bytes() allocates the whole body: one
name, the path that does not allocate. JacksonExtension is JsonExtension, and
autoJson() is auto().

The root POM now manages every module of this build, so anything composing
Flash imports it once and never names a version again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Relism merged commit 2f06ca7c1d into master 2026-09-23 14:11:17 +00:00
Relism deleted branch feature/handlers/typed-bodies-and-injected-services 2026-09-23 14:11:17 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Relism/Flash5#20