The compiler ignored the message a constraint declares and always wrote its
own, so a failed @Pattern answered the caller with a regex. It now uses the
annotation's message whenever one is set, and keeps the plain description for
jakarta's default, which is a resource bundle key and not something to put in
front of whoever sent the request.
This is what makes moving a check out of a service and onto the type it
belongs to cost nothing: the wording moves with 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>
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>