Skip to content

JAX-RS compatibility

View Markdown

Vertique’s REST layer is authored entirely with JAX-RS 4.0 annotations (jakarta.ws.rs-api 4.0.0), but the runtime underneath is Vertique’s own Vert.x-native engine. It is not Jersey, RESTEasy, or any other container. JAX-RS is the authoring surface, a familiar and specified vocabulary for describing an HTTP resource, and nothing underneath expects a full JAX-RS container. The surface therefore splits three ways:

  • Supported. Behaves as the specification says: resources and verbs, parameter binding, ExceptionMapper, Response.
  • Deliberately replaced. A Vert.x-shaped equivalent exists: filters and providers become Dagger-wired interceptors and codecs, @Suspended becomes returning a future, and the JAX-RS client becomes the declarative REST client.
  • Not implemented. For each of these, this page states what happens if you use it anyway.

The table below summarizes each area, and each section then gives the behavior in detail. Everything applies the same way whether a resource is discovered by the reflective scanner at startup or compiled ahead of time by the annotation processor. The two paths are built to match exactly.

Area Verdict Detail
Resources and methods Supported, with one gap HTTP-verb annotations, @Produces/@Consumes, and regex-constrained @Path templates all work; sub-resource locators do not exist.
Parameters Supported, with one gap Every standard parameter-binding annotation except @MatrixParam works on a resource method, and a real ParamConverterProvider is consulted for scalar conversion.
Bodies and entities Diverges Body reading and writing run through a framework decoder/encoder SPI, not MessageBodyReader/MessageBodyWriter; multipart EntityPart is supported within that model.
Responses Supported, with gaps Response and @Produces negotiation work as specified, though the builder’s variant methods throw; StreamingOutput is not recognized; there is no response-body contract validation.
Providers and filters Diverges ContainerRequestFilter, ContainerResponseFilter, ReaderInterceptor, WriterInterceptor, @Provider, and @NameBinding are all replaced by framework interceptor and codec extension points wired through Dagger.
Exception handling Supported ExceptionMapper and WebApplicationException behave as specified, backed by a hierarchy-aware registry.
Context injection Supported, with a reserved list @Context resolves the Vert.x routing context, the JAX-RS SecurityContext, and framework context types; seven standard JAX-RS context types are rejected at startup.
Validation Diverges by default The default request-validation path enforces Bean Validation annotations as a synthesized JSON Schema, not as Jakarta Validation; a genuine Bean Validation runtime is available as a separate opt-in.
Async model Diverges A resource method returns a Future; @Suspended and AsyncResponse are never recognized.
Resilience on a resource Vertique extension @Resilient and @Timeout bound a concrete Dagger-managed resource method; neither is part of standard JAX-RS.
Client API Not supported jakarta.ws.rs.client is not implemented; a separate declarative REST client module is the framework’s alternative for calling other HTTP services.

Every standard HTTP-verb annotation is supported: @GET, @POST, @PUT, @DELETE, @PATCH, @HEAD, and @OPTIONS all route through the same scan, whether it runs reflectively or at compile time. @HEAD additionally has its entity stripped from the response after conditional-request evaluation, matching HTTP semantics. @Produces and @Consumes are read at both class and method level (method overrides class) and are enforced. An incoming request whose Content-Type does not satisfy the operation’s @Consumes is rejected with 415 before validation or the resource method runs, and @Produces drives Accept-header content negotiation on the way out (see Responses below). The annotation is read with one divergence: each array element must hold exactly one media type. A comma-separated list inside a single string is not split and is treated as one invalid media type. Declare multiple media types as separate array elements.

A plain @Path template such as /items/{id} is translated into a Vert.x route with a named path parameter. A regex-constrained template such as /items/{id:[0-9]+} is supported too: the whole template compiles to a single anchored regular expression with one named capture group per variable, so path-parameter binding by name works the same way.

Sub-resource locators are not supported. A sub-resource locator is a method that carries @Path but no HTTP-verb annotation and returns another resource instance for further dispatch. A method with zero verb annotations is treated as a non-endpoint method and silently skipped: no route is registered for it, and nothing is logged or reported at startup. A resource method written as a sub-resource locator never receives a request, and there is no compile-time or startup signal that it was ignored.

Every JAX-RS parameter-binding annotation except one is supported on a resource method: @PathParam, @QueryParam, @HeaderParam, @CookieParam, @FormParam, and @BeanParam all bind as specified, with @DefaultValue honored on every scalar form. @MatrixParam is the exception. It is recognized only in the declarative REST client’s request interfaces (see Client API) and in the framework’s UriBuilder implementation for constructing URIs. It is not recognized on a server resource method. A @MatrixParam-annotated resource-method parameter is not rejected: the parameter scanner does not recognize the annotation, so the parameter falls through the same path an unannotated parameter takes and is bound as the request body. If the method already declares a real, unannotated body parameter, that combination fails at startup over the duplicate body binding. Otherwise the framework silently tries to deserialize the request body into whatever type the @MatrixParam parameter declared.

Scalar type conversion is layered. A built-in native converter registry covers String, the primitive and boxed numeric types, boolean, char, BigInteger, BigDecimal, UUID, URI, every java.time value type, and any enum (converted by exact constant name). Beyond that registry, a standard jakarta.ws.rs.ext.ParamConverterProvider contributed by the application is consulted at request time, in ascending @Priority order, and the first non-null result wins. A parameter type that neither the registry nor any registered provider can satisfy is rejected at startup, before the application serves a request.

Request-body reading and response-body writing run entirely through two framework SPIs: a decoder chain for reading and an encoder chain for writing. JAX-RS’s own MessageBodyReader/MessageBodyWriter contracts are not part of the model. Nothing in the framework looks for one, and contributing one has no effect.

The decoder chain ships four built-in decoders, tried in priority order. The first whose declared Content-Type and target type both match wins. The four are:

  • a text decoder for any text/* body into a String;
  • a binary decoder for application/octet-stream into a Buffer or a byte[];
  • a form decoder for application/x-www-form-urlencoded into any plain object;
  • a JSON decoder as the fallback for everything else, including a raw JsonObject.

An application-contributed decoder runs before all four by default, so it can intercept a content type the framework would otherwise handle.

Multipart is supported, including the JAX-RS EntityPart representation alongside the plain Vert.x file-upload type. A resource method can declare a named EntityPart parameter, a named list of them, or an unannotated aggregate list covering every part of the request, both file-backed parts and text-field parts. This support has one limit: the framework’s own file-part size/content-type constraint annotation is accepted only on the plain file-upload type and not on an EntityPart parameter. An EntityPart may represent a text field the constraint mechanism cannot see, so accepting the annotation there would open a bypass.

Streaming applies to responses only. Before any decoder or your resource method runs, the router’s body handler always reads the request body fully and, for multipart, spools it to disk. There is no way to receive a live, unbuffered inbound stream. A response can stream: a resource method may return a live byte stream that is piped to the client as it produces data, and a dedicated mechanism is available for Server-Sent Events.

jakarta.ws.rs.core.Response is a first-class return type, built the normal way and returned either directly or wrapped in a Future. Because the runtime is not built on Jersey or RESTEasy, vertique-rest-jaxrs ships its own minimal RuntimeDelegate. It provides just enough for the standard Response builder, UriBuilder, and Link to work standalone, and anything a full JAX-RS container would additionally provide is out of scope. The builder itself has one limit: the variants(...) methods throw UnsupportedOperationException, so variant-based negotiation cannot be expressed through the builder.

@Produces content negotiation works against the request’s Accept header. Candidate media types come from the operation’s @Produces list, or a JSON default when none is declared, and are matched by specificity and quality value. An unsatisfiable request is answered with 406, the same outcome a compliant JAX-RS container would produce. This negotiation is media-type only, with no charset or language negotiation layered onto it.

Two things a JAX-RS-literate reader might expect are missing. First, StreamingOutput is not implemented. The equivalent is returning a live stream directly from the resource method, as described above. Second, neither of the framework’s request-validation strategies validates a resource method’s response against any schema or contract. Both gate the request only. A response body that violates its own declared shape is not caught by the framework, and an application that needs that guarantee has to check it itself.

None of the classic JAX-RS provider or filter contracts are implemented: ContainerRequestFilter, ContainerResponseFilter, ReaderInterceptor, WriterInterceptor, the @Provider registration annotation, and @NameBinding do not exist anywhere in the runtime. The divergence is deliberate: the framework replaces the whole family with its own Dagger-wired interceptor and codec extension points, detailed in full in the rest-core module reference below. The rough correspondence, for a reader coming from standard JAX-RS:

Instead of Reach for
ContainerRequestFilter / ContainerResponseFilter a request interceptor, contributed through a Dagger multibinding, with hooks that run before dispatch and after the response is produced
ReaderInterceptor the request body decoder chain described under Bodies and entities
WriterInterceptor the response body encoder chain
@Provider a Dagger multibinding contribution. Registration happens at compile time, not by classpath scanning
@NameBinding nothing declarative; a per-operation interceptor inspects the operation’s own metadata to decide whether to act, or a handler contributor is placed at a specific priority range instead

An additional per-operation interceptor is also available. It runs once per matched resource-method invocation instead of once per request at the router level, and it has its own before, after, and recover hooks around the method call. It is the closest match to a JAX-RS filter scoped to one resource method, but it is contributed the same way, through a Dagger multibinding. It does not use a name-binding annotation.

jakarta.ws.rs.ext.ExceptionMapper is supported as specified: implement it for your exception type and contribute it through a Dagger multibinding, and it is consulted ahead of the framework’s own defaults for that same type. Lookup is hierarchy-aware (a mapper registered for a superclass still matches a thrown subclass), and results are cached after the first lookup.

WebApplicationException behaves as specified too: its carried Response is used directly, with the original entity preserved when present. A long list of the framework’s own exception types already has a mapper contributed out of the box, including validation failures, not-found, conflict, unauthorized, forbidden, unavailable, and a few more. An uncaught, unmapped throwable of any other type still gets a well-formed problem-details response, so the client does not see a raw stack trace or a hung connection.

@Context resolves a fixed, framework-chosen set of types and does not resolve every type a JAX-RS container might supply. The set is the Vert.x routing context, the JAX-RS SecurityContext (the exact interface only, not a custom subtype), and any of the framework’s own context-value types: its own security context, correlation data, localization, and similar request-scoped application values. All of them resolve through the same ordered resolver chain, and an application can contribute its own resolver for a custom context-value type.

Seven standard JAX-RS context types are explicitly not supported: UriInfo, HttpHeaders, Request, Configuration, Application, Providers, and ResourceContext. These are recognized as syntactically valid @Context targets, but a resource method declaring one of them fails at startup with a clear error. It does not start successfully and then inject a null value the first time the method runs. The same fail-fast treatment applies to a @Context parameter combined with a JAX-RS value-binding annotation on the same parameter, and to a @Context parameter whose declared type is neither a built-in injectable type nor an application context-value type. Both are startup errors.

Bean Validation annotations on a request body or parameter are read by default, but they are not enforced by a Jakarta Validation (JSR 380) engine. The default request-validation path reads the same annotations JAX-RS and Bean Validation put on a resource method and its request types and synthesizes a JSON Schema fragment per operation from them, once, at startup. Every request is then validated against that synthesized schema, and the original annotations are not evaluated at request time. Most of the time the two enforcement models agree, but they are not identical, and the difference is observable. JSON Schema’s pattern keyword is an unanchored search that matches if the pattern is found anywhere in the value, while a real Bean Validation @Pattern match is fully anchored against the whole value. A constraint requiring at least one non-whitespace character, for example, behaves like a not-blank check under the framework’s default JSON-Schema-backed path, but would reject any longer value outright under a genuine Bean Validation runtime.

A genuine Jakarta Validation runtime is also available as a separate, opt-in module. Once it is included in the application’s dependency-injection graph, it runs constraint validation with Bean Validation semantics against resource-method arguments, after parameters are extracted and before the resource method is invoked. The two mechanisms are independent and can both be active on the same application at once: the schema gate runs first as part of request validation, and the opt-in Bean Validation runtime runs second as part of method dispatch. Response bodies are validated by neither mechanism.

See REST APIs for how the default schema-based path is wired and configured, and the vertique-validation module reference below for the separate Bean Validation runtime.

@Resilient is a Vertique AOP annotation and is not part of standard JAX-RS. For an inbound resource, the documented use is to put it on a concrete Dagger-managed resource method together with @Timeout. The generated proxy then bounds the asynchronous execution. This page does not recommend retry, circuit-breaker, or bulkhead policy at the inbound resource boundary. Those policies belong on the outbound client or service operation that owns the corresponding failure and replay semantics.

JAX-RS routing and AOP have separate responsibilities. The route performs authentication, content-type checks, and request validation before invoking the resource. The AOP proxy wraps the resource method and its downstream asynchronous work. To enable the path, include the generated GeneratedAopModule and GeneratedJaxRsResourcesModule together with ResilienceModule and ResilienceAopModule in the component. The REST APIs timeout section shows the resource-side setup.

A resource method’s return type is Future. A synchronous return works too, but every non-trivial resource method that does I/O returns a Future of its result, resolved asynchronously before the response is serialized. A Future that completes with no value sends 204 with no body, matching a void method. That is the only asynchronous model available. The standard JAX-RS asynchronous pattern, a @Suspended AsyncResponse parameter that the method resumes later from another thread, is not implemented. Both the annotation and the type ship with the Jakarta WS-RS API dependency, so the declaration compiles, but no code path recognizes either, and the parameter falls through the same unannotated-parameter body binding described under Parameters. On a Vert.x-native runtime, a Future-returning method already expresses the same intent without a second, parallel mechanism.

A standard Jakarta REST Application subclass is not supported as a way to group resources: it is not registered, and nothing in it takes effect. A named application is declared with @RestApplication on an interface. See Several APIs for how declaring one changes mounting.

Concern Verdict Detail
Application subclass Not registered The class compiles with one build warning naming it, which a build that treats warnings as errors turns into a failure; @NoAutoWire on the class silences the warning. No route ever answers under it.
@ApplicationPath No effect Nothing is mounted at its value.
getClasses() No effect Its return value selects nothing.
getSingletons() No effect Its return value selects nothing.
Its resources Served elsewhere At jaxrs.basePath on the legacy default mount when no @RestApplication is declared, otherwise only where a declared application selects them.
Named applications Vertique replacement A @RestApplication interface names the application, its path, and either its listed resources or a sole discover = true; the interface is never implemented or instantiated.

Porting a subclass keeps its path: declare a @RestApplication interface with the same path that lists the subclass’s resources, or sets discover = true when it is the sole declaration, then delete the subclass. Until then, rules keyed on the old @ApplicationPath prefix, such as gateway routes and allowlists, stop matching its resources.

See the @RestApplication section of the vertique-rest-jaxrs module reference for membership, lifecycle, and mount-conflict rules, and the vertique-codegen-jaxrs module reference’s porting section for a before-and-after port and the build warning’s exact text.

jakarta.ws.rs.client (Client, ClientBuilder, WebTarget, and the rest of the JAX-RS client API) is not implemented and is not the framework’s model for calling another HTTP service. The alternative is a separate, declarative REST client: annotate a plain Java interface with @RestClient plus the same JAX-RS annotations used on a server resource method (@GET, @Path, @QueryParam, and so on), build it once through the client factory, and call its methods like any other injected service. Each call becomes an asynchronous HTTP request over a Vert.x-backed web client and shares the same parameter-conversion stack the server side uses. This is an outbound client feature and does not provide JAX-RS client compatibility. See REST clients for its annotation surface, resilience, configuration, and request-execution pipeline.