Kotlin framework for generated REST clients.
Sunday is delivered as a set of Maven artifacts and releases are available from Maven Central.
Since 1.0.0-beta.24, the previous monolithic sunday artifact has been split into modules.
sunday-core: Core types, client abstractions, and credential lifecycle contracts.sunday-validation-jakarta/sunday-validation-javax: Native Bean Validation adapters; choose the namespace used by your application.sunday-validation-core: Shared schema constraint support used by both validation adapters.sunday-okhttp: OkHttp-based transport implementation.sunday-jdk: JDKHttpClient-based transport implementation.sunday-jaxrs-quarkus: Quarkus REST/JAX-RS support utilities.sunday-client-quarkus: Native named OIDC acquisition and bounded recovery for generated Quarkus clients.sunday-broker: Protocol-neutral broker operation support for generated AsyncAPI clients.sunday-problem: Default RFC7807 problem type (SundayHttpProblem) and adapters.sunday-problem-quarkus: QuarkusHttpProblemintegration.sunday-problem-zalando: Zalandoproblemintegration.
implementation("io.outfoxx.sunday:sunday-core:$version")
implementation("io.outfoxx.sunday:sunday-okhttp:$version") // or sunday-jdk
implementation("io.outfoxx.sunday:sunday-jaxrs-quarkus:$version") // Quarkus REST/JAX-RS server support
implementation("io.outfoxx.sunday:sunday-problem:$version") // or sunday-problem-quarkus / sunday-problem-zalando<dependency>
<groupId>io.outfoxx.sunday</groupId>
<artifactId>sunday-core</artifactId>
<version>${sunday.version}</version>
</dependency>
<dependency>
<groupId>io.outfoxx.sunday</groupId>
<artifactId>sunday-okhttp</artifactId>
<version>${sunday.version}</version>
</dependency>
<dependency>
<groupId>io.outfoxx.sunday</groupId>
<artifactId>sunday-problem</artifactId>
<version>${sunday.version}</version>
</dependency>Transport and problem modules now register SPI providers for automatic discovery.
Use DefaultFactories to create instances without wiring implementations manually:
val transport = DefaultFactories.transport(
URITemplate("https://api.example.com"),
)If multiple providers are on the classpath, specify which one to use:
val transport = DefaultFactories.transport(
URITemplate("https://api.example.com"),
providerId = "okhttp", // "okhttp" or "jdk"
)
val problemFactory = DefaultFactories.problemFactory(
providerId = "quarkus", // "sunday", "quarkus", or "zalando"
)Problem providers are chosen by highest priority when multiple are present; if there is a
tie, or if multiple transport providers are present, specify providerId explicitly.
sunday-broker provides Flow<BrokerRawDelivery>.decodeDeliveries for consumers that
need to handle an undecodable message and then continue consuming. It accepts a
BrokerDecodeFailureHandler with the consume specification, original raw delivery,
and decoding exception. Without a handler, it rethrows the decoding exception.
The handler owns recovery and settlement. Returning skips the failed delivery and allows consumption to continue; throwing stops collection. The operator never acknowledges or negatively acknowledges a delivery itself. If recovery transfers the message to durable quarantine, the handler must wait for the required transfer confirmation before acknowledging the original delivery. A failed transfer must propagate, leaving reconnect/requeue policy to the application transport.
Only decoding failures reach this handler. Transport errors and downstream processing errors propagate without invoking recovery. Cancellation and JVM errors also propagate, including when wrapped by a codec. Handlers should honor coroutine cancellation and must not swallow failed transfers. Retry policy, quarantine storage, and reconnection remain application responsibilities, not runtime defaults.
- New problem abstraction (
Problem,ProblemFactory,ProblemAdapter) decouples Sunday from any specific problem library while still allowing integration modules. - Failure responses now decode RFC7807 problems into registered types or
ProblemFactory-built problems; non-problem error bodies are attached asresponseTextorresponseDataextensions. - Added
io.outfoxx.sunday.http.Statusto model HTTP status codes and reason phrases consistently across core and problem modules.
Generated clients select logical security bindings from the explicitly chosen profile.
Register the matching provider names on the application's TokenManager, and supply that
manager to the transport. Credentials and token storage stay in application configuration.
val provider = JdkOAuthTokenProvider(
OAuthTokenProvider.Configuration(
identity = "external-service",
clientId = applicationClientId,
clientSecret = applicationClientSecret,
authentication = OAuthTokenProvider.Authentication.ClientSecretBasic,
),
)
val tokens = TokenManager(mapOf("external" to provider))
val transport = DefaultFactories.transport(
URITemplate("https://api.example.com"), providerId = "jdk", tokenManager = tokens,
)Use OkHttpOAuthTokenProvider with the OkHttp backend. Both native providers support
client credentials and application-managed authorization code/PKCE. Interactive sessions
need a distinct grantIdentity and an authorization callback returning a fresh code,
redirect URI, and PKCE verifier. Failed refreshes require fresh application authorization;
a consumed authorization code is never reused. External/static credentials can implement
TokenProvider directly. Implement TokenProvider.Refreshing when renewal is supported.
TokenManager partitions credentials by provider/client identity, profile, acquisition
endpoints, scopes, audience/resource, and grant identity. It coalesces concurrent renewal,
applies expiry skew, preserves refresh-token rotation, and cancels acquisition when its
last waiting caller cancels. Close it when the owning application/session ends. Supply a
TokenStore for application-managed persistence; tokens must not appear in generated code.
Endpoint overrides change acquisition only; discovery verifies the independently configured
issuer on acquisition and renewal. Public authorization-code/PKCE sessions configured with
Authentication.None accept discovery that omits none (as Keycloak does), including an
absent authentication-method list. Any supplied list must still contain only strings.
Confidential clients require their configured method to be advertised; an absent list
defaults to client_secret_basic. Endpoint overrides never bypass issuer validation.
Managed requests disable native redirects and ambient authentication. An explicit
Bearer invalid_token challenge can recover once for a bodyless GET, HEAD, or OPTIONS.
403 responses and unsafe/body-carrying requests do not replay. An event subscription shares
that single recovery across reconnects; closing it cancels pending acquisition.
Generated Kotlin models use Bean Validation rather than a second generated validation API.
Use validator.validate(model, ModelMode.Request::class.java) for client sends/server inputs
and ModelMode.Response::class.java for server outputs/client reads. Both groups include
ordinary schema constraints. Unknown declared fallbacks are strict in request mode unless
the schema explicitly allows them there. Constructors use response mode. Always select a payload
mode explicitly: dynamic extension containers cascade that mode to their current model values,
including values inside nested lists and maps. The native Default group alone does not select
a payload mode. Bind ModelValidation.validatorProvider when using a customized validator so
constructor and dynamic-container adapters share the application's configuration.
Select io.outfoxx.sunday.validation.jakarta.ModelMode with Jakarta/Hibernate Validator 8+
or the javax package with Hibernate Validator 6. A compatible Bean Validation provider
must be present. Framework integrations bind ModelValidation.validatorProvider to their
managed Validator; standalone applications may use its default factory. Generated request
hooks validate current values on every execution, including deferred operations.
Profiled generated clients use sunday-client-quarkus and application-configured named OIDC
providers. Quarkus owns token acquisition, caching, expiry renewal, and refresh-token rotation.
The generated configuration source isolates clients by the selected profile, endpoints, and scopes;
credentials remain in application configuration.
Call the generated public operation. Its native WithRetry boundary and concrete Transport
method share one ClientInvocation; no second retry loop is added. Authentication can recover
once for a bodyless GET, HEAD, or OPTIONS with a Bearer invalid_token challenge. Recovery
consumes the declared native retry budget, or one authentication-only retry when no policy is
declared. Repeated 401s, 403s, unsafe requests, and missing challenges never cause an additional
authentication replay. Applications receive the original native exceptions.
Each deferred subscription receives a fresh invocation. Cancellation reaches the native
completion-stage or reactive transport. Streamed subscriptions are not automatically replayed.
Generated helpers are implementation boundaries; native method-specific policy configuration
must name the corresponding WithRetry or Transport method.
Copyright 2021 Outfox, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Credentials are isolated by logical security scheme as well as provider and acquisition inputs. Discovery metadata is fetched and verified on each acquisition or renewal. Temporary provider outages allow event connections to reconnect; a rejected refresh grant triggers fresh client credentials only for the client-credentials flow. Interactive sessions require fresh application authorization. Built-in OAuth providers retain at most 1,024 consumed authorization-code hashes per provider instance. After this limit, create a provider for a newly authorized application session; old hashes are never evicted to allow code reuse. Refresh exchanges do not consume this history.
OperationSpec.parameterValidation and the transport overloads accept a ParameterValidator for
captured typed parameters. JDK and OkHttp transports invoke it before encoding on every request build,
including bodyless requests and event streams. Custom transports must invoke the callback at that same
boundary. Generated callbacks delegate to native Bean Validation in request mode.
The JAX-RS ClientModelValidation provider validates generated tolerant scalar parameters, including
Kotlin covariant collection elements, during wire conversion. Register it on non-MicroProfile clients;
generated MicroProfile clients register it automatically. Server parameter constraints use the native
validator before application invocation.
Parameter validation failures throw ParameterValidator.Failure with the native error as the cause.
Event sources close on this failure, and event flows fail instead of reconnecting. Custom transports
should invoke ParameterValidator.validateRequest() before encoding typed parameters.
Use UpdateOp<T> for fields that can be set but not deleted, and PatchOp<T> for fields that can
also be deleted. PatchOp.none() leaves a field unchanged, PatchOp.set(value) supplies an update,
and PatchOp.delete() writes JSON null to delete a member. Choose the operation type according to
whether the member may be removed, independently of whether its value may be null. Annotate patch models with Jackson's
@JsonInclude(NON_EMPTY) to omit unchanged fields. Jackson rejects JSON null for UpdateOp with a
mapping error and preserves it as deletion for PatchOp, including root and collection positions.
URITemplate.resolve expands missing and null variables as undefined under RFC 6570.
For an undefined id, /items{/id} becomes /items, while /items/{id} becomes
/items/ because the literal slash remains. Empty strings remain defined: an empty
id in /items{/id} produces /items/. Explicit null overrides a parameter
stored on the template; omitting the override keeps the stored value. This applies
to template expressions in both the base URI and the operation path.
val settings = ClientSettings.resolve(
baseURL, alternatives, credentials,
tokenManagerFactory = { providers ->
TokenManager(providers, store = applicationStore, expirySkew = Duration.ofSeconds(30),
clock = applicationClock, scope = applicationScope)
},
)The same optional tokenManagerFactory: TokenManagerFactory? is available on the settings constructor.
The application closes settings.tokenManager when all clients sharing it are done. Closing a transport
does not close the shared manager or erase its store. TokenManager.close() cancels its own jobs without
canceling the application parent scope; let pending operations/commits settle before erasing storage.
The hook is invoked once with the resolved provider map, after security validation, and is skipped when no providers are selected. It must only construct a manager: do not acquire tokens or read storage in the hook. Omitting it keeps the existing in-memory default. Settings retain the returned manager, not the factory. All operations on those settings share it; generated aggregate children therefore retain the same cache and single-flight renewal. Independently created managers do not coordinate concurrent refreshes, even if their stores are the same. Reuse a client/aggregate within an active session; use successive managers to reopen saved sessions.
The application owns persistence, encryption, store access and session boundaries. Provider/client,
grant, profile and endpoint identities must distinguish environments and users; the API base URL
alone is not an implicit store namespace. Use a new grant identity for a fresh authorization session.
For logout, stop requests and wait for pending refresh/persistence to finish before removing the
session's store entries, then construct fresh settings. invalidate expires an access token for
renewal; it is not logout and deliberately retains refresh state. No disk storage is enabled automatically.