OpenAPI codegen
This module generates Kora code from an OpenAPI contract using OpenAPI Generator.
From a single API description, it can create declarative HTTP server handlers or declarative HTTP clients,
as well as request and response models, mappers, authorization handling, and additional annotations.
This approach is useful when OpenAPI is the source of truth for the transport contract and application code must follow it automatically.
For a step-by-step walkthrough before the reference documentation, see OpenAPI HTTP Server, Advanced OpenAPI HTTP Server, and OpenAPI HTTP Client.
Dependency¶
Generator dependency in build.gradle:
Plugin dependency in build.gradle:
Other plugin versions are not guaranteed to work because the OpenAPI Generator API can be incompatible at code level.
Dependency in build.gradle.kts:
Plugin dependency in build.gradle.kts:
Other plugin versions are not guaranteed to work because the OpenAPI Generator API can be incompatible at code level.
The io.koraframework:openapi-generator artifact is added to the buildscript classpath, so it is loaded by the Gradle JVM itself rather than by the compiled application.
Kora is compiled for JDK 25, therefore the Gradle daemon must also run on JDK 25 or newer, otherwise generation fails with UnsupportedClassVersionError before any project code is compiled.
Setting only the project toolchain is not enough — the toolchain applies to compilation, not to the Gradle JVM.
Generated code also requires the HTTP server or HTTP client module, depending on the selected generation mode, plus the JSON module, and the validation module when server validation is enabled.
Configuration¶
Configure the OpenAPI Generator plugin parameters:
Gradleplugin parameters are described in the plugin documentation.- The
configOptionsplugin parameter is described in the configuration documentation. - The
openapiNormalizerplugin parameter is described in the customization documentation.
The Kora generator is selected with generatorName = "kora" and the target artifact is selected with configOptions.mode.
Kora supports exactly four modes:
| Mode | Generates |
|---|---|
java-client |
Java declarative HTTP client interfaces, models and mappers |
java-server |
Java HTTP server controllers, delegate contracts and mappers |
kotlin-client |
Kotlin declarative HTTP client interfaces, models and mappers |
kotlin-server |
Kotlin HTTP server controllers, delegate contracts and mappers |
Generated code is synchronous: client methods return the response value, and delegate methods return the response value.
An unknown mode value fails generation with a message listing the supported modes.
Common OpenAPI Generator Options¶
In addition to Kora-specific configOptions, GenerateTask accepts common OpenAPI Generator parameters.
They define where to read the contract from, where to put generated files, which packages to use, and how to preprocess the OpenAPI description.
For Kora projects, these parameters are usually set explicitly because generated code is then added to normal project compilation.
| Parameter | Description |
|---|---|
generatorName |
Generator name (required, no default). Always set it to kora for Kora. |
inputSpec |
Path to the OpenAPI file (required, no default). Usually this is a file under src/main/resources/openapi, for example $projectDir/src/main/resources/openapi/openapi.yaml. |
outputDir |
Directory for generated files (not specified by default, optional). In Kora projects, this is usually a directory under build, for example $buildDir/generated/openapi, and it is added to the main source set. |
apiPackage |
Package for generated API interfaces, controllers, delegate classes, and mappers (default: org.openapitools.api). It is recommended to set it explicitly, for example io.koraframework.example.openapi.api. |
modelPackage |
Package for models generated from OpenAPI schemas (default: org.openapitools.model). It is recommended to set it explicitly, for example io.koraframework.example.openapi.model. |
invokerPackage |
Auxiliary generator package (default: org.openapitools.api). It is recommended to set it explicitly next to apiPackage and modelPackage, for example io.koraframework.example.openapi.invoker. |
configOptions |
Generator-specific parameters (default: {}). For Kora, this is where mode, clientConfigPrefix, enableServerValidation, extensions, and the other parameters described below are set. |
globalProperties |
Limits which entities are generated (default: {}). Useful when you need to generate only apis, only models, or specific models and operations. Use carefully: normal Kora clients and servers usually need API classes, models, and mappers together. |
openapiNormalizer |
Preprocesses the OpenAPI contract before generation (default: {}). Often used to disable standard transformations with DISABLE_ALL, generate only selected operations with FILTER, or control rules such as SIMPLIFY_ONEOF_ANYOF. |
importMappings |
Maps a schema name to an existing class (default: {}). Useful when a model is written manually or comes from another module, for example Money: "com.example.Money". |
typeMappings |
Maps an OpenAPI Generator type to a language type (default: {}). Used for targeted type replacement, for example replacing OffsetDateTime with a project-specific time type. |
schemaMappings |
Maps an OpenAPI schema to an external type without generating the model (default: {}). Similar to importMappings, but configured at schema level and useful for reusing shared DTOs. |
skipValidateSpec |
Skips OpenAPI contract validation before generation (default: false). In normal builds it is better to keep validation enabled; use true only temporarily for external contracts that cannot be fixed quickly. |
cleanupOutput |
Cleans outputDir before generation (default: false). Useful when the contract changes often and files from removed operations or models must disappear. Do not point outputDir to a directory with handwritten code. |
Example with common options:
def openApiGenerateHttpClient = tasks.register("openApiGenerateHttpClient", GenerateTask) {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml"
outputDir = "$buildDir/generated/openapi/client"
def corePackage = "io.koraframework.example.openapi"
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
skipValidateSpec = false
cleanupOutput = true
openapiNormalizer = [
DISABLE_ALL: "true",
FILTER: "tag:public|billing"
]
configOptions = [
mode: "java-client",
clientConfigPrefix: "httpClient.billing",
filterWithModels: "true"
]
}
val openApiGenerateHttpClient = tasks.register<GenerateTask>("openApiGenerateHttpClient") {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml"
outputDir = "$buildDir/generated/openapi/client"
val corePackage = "io.koraframework.example.openapi"
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
skipValidateSpec = false
cleanupOutput = true
openapiNormalizer = mapOf(
"DISABLE_ALL" to "true",
"FILTER" to "tag:public|billing"
)
configOptions = mapOf(
"mode" to "kotlin-client",
"clientConfigPrefix" to "httpClient.billing",
"filterWithModels" to "true"
)
}
Use globalProperties only for narrow generation tasks, for example when extracting a few models into an intermediate module:
Useful openapiNormalizer Rules¶
openapiNormalizer changes the input OpenAPI contract before generation. It is not a Kora parameter, but a general OpenAPI Generator mechanism.
For Kora, it is especially useful when one large contract is used by several applications or when the contract contains ambiguous shapes for code generation.
| Rule | Description |
|---|---|
DISABLE_ALL |
Disables standard normalization rules (default: false). Starting with OpenAPI Generator 7, some rules are enabled by default, so predictable generation often starts with DISABLE_ALL: "true" and then enables only the needed rules explicitly. |
FILTER |
Keeps only selected operations for generation (not specified by default, optional). Supports one filter at a time: operationId:name1\|name2, method:get\|post, or tag:public\|billing. Operations that do not match are marked as x-internal: true and are not generated. |
KEEP_ONLY_FIRST_TAG_IN_OPERATION |
Keeps only the first tag on an operation (default: false). Useful when operations have several tags and are split into several API classes differently from what you expect. |
SET_TAGS_FOR_ALL_OPERATIONS |
Replaces tags on all operations with one provided value (not specified by default, optional). Useful when you want to force one generated API class. |
SET_TAGS_TO_OPERATIONID |
Sets an operation tag to operationId, or to default when operationId is empty (default: false). Useful for contracts without usable tags when predictable operation grouping is needed. |
SET_TAGS_TO_VENDOR_EXTENSION |
Reads operation tags from the specified extension, for example x-tags (not specified by default, optional). Useful when an external contract cannot be changed but already has custom operation grouping. |
FIX_DUPLICATED_OPERATIONID |
Adds a numeric suffix to duplicated operationId values (default: false). It is better to fix the contract, but this rule helps generate code for an external description temporarily. |
SET_BEARER_AUTH_FOR_NAME |
Converts the specified security scheme to bearerAuth (not specified by default, optional). Useful for external contracts where a bearer token is described in a non-standard way but should be handled as a normal bearer scheme in the application. |
REF_AS_PARENT_IN_ALLOF |
Marks a $ref inside allOf as a parent schema with x-parent: true (default: false). Can help contracts that model inheritance through allOf. |
SIMPLIFY_ONEOF_ANYOF |
Simplifies some oneOf/anyOf constructs, for example by moving a null variant to nullable: true and removing single wrappers (enabled by default in OpenAPI Generator 7 unless DISABLE_ALL is set). For Kora, this can change generated model shapes, so enable it deliberately. |
SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING |
Simplifies anyOf made from string and a string enum to string (default: false). This can help with contracts where the enum restriction is not important for code. |
SIMPLIFY_BOOLEAN_ENUM |
Converts a boolean enum to a plain boolean (enabled by default in OpenAPI Generator 7 unless DISABLE_ALL is set). |
REFACTOR_ALLOF_WITH_PROPERTIES_ONLY |
Moves properties from a schema that has both allOf and properties into a separate schema inside allOf (enabled by default in OpenAPI Generator 7 unless DISABLE_ALL is set). This can help inheritance, but strict contracts should be checked after generation. |
NORMALIZE_31SPEC |
Normalizes some OpenAPI 3.1 constructs into a form better understood by the generator (default: false). Useful for 3.1 contracts when generation fails on newer schema forms. |
REMOVE_X_INTERNAL |
Removes x-internal: true from operations and models (default: false). Use only when the contract already contains x-internal, but a specific generation task must force such operations back in. |
SET_CONTAINER_TO_NULLABLE |
Marks container types array, set, or map as nullable (not specified by default, optional). Use only when an external contract systematically misses nullable on such fields. |
SET_PRIMITIVE_TYPES_TO_NULLABLE |
Marks primitive types string, integer, number, or boolean as nullable (not specified by default, optional). This significantly changes model signatures, so apply it only to problematic external contracts. |
Example of generating only the public part of a contract:
FILTER excludes only operations by itself. If unused models should also be removed after filtering, enable the Kora filterWithModels parameter.
For more complex selection, usually create separate generation tasks with different FILTER values, for example one with tag:billing and another with operationId:createUser|getUser.
Example of normalizing tags for a contract without convenient grouping:
Model and Body Options¶
These configOptions shape the generated models and the handling of untyped request and response bodies.
They do not depend on whether a client or a server is generated.
| Parameter | Description |
|---|---|
rawBodyMode |
Type used for a request or response body described as a bare type: object without properties (default: BYTES). BYTES generates byte[] / ByteArray, BODY generates streaming HttpBodyOutput / HttpBodyInput, OBJECT generates Object / Any serialized as JSON. |
filterWithModels |
Removes models that became unused after openapiNormalizer.FILTER (default: false). See Model Filtering. |
extensions |
JSON object with additional model, enum, method and type annotations and with interceptors (not specified by default, optional). See Generator Extensions. |
JSON mappers are always bound with io.koraframework.json.common.annotation.Json and are generated by the JSON annotation processor,
so no annotation-name option is needed.
A bare type: object used as a model property is always generated as Object / Any regardless of rawBodyMode, which only affects request and response bodies.
When a bare-object body is generated as BYTES or BODY, the generator also adds an @Header HttpHeaders argument in front of the body,
so the caller can set Content-Type and any other transport headers that are no longer derivable from the contract.
Generator Extensions¶
extensions is a single JSON option that attaches annotations and interceptors to generated code.
It has three sections, all optional:
*— applied to every operation and to every generated model and enum typetags— keyed by theOpenAPItag name, applied to the operations of that tagoperations— keyed byoperationId, applied to that single operation
Every section accepts the same fields:
| Field | Description |
|---|---|
additionalMethodAnnotations |
Annotations added above generated client methods and server controller methods. A string or an array of strings. Supports the %{configPath} placeholder. |
additionalTypeAnnotations |
Annotations added to generated model types and enum types. Only the * section is used for type annotations. |
additionalModelTypeAnnotations |
Annotations added to generated model types only. Only the * section is used. |
additionalEnumTypeAnnotations |
Annotations added to generated enum types only. Only the * section is used. |
interceptorType |
Interceptor implementation class placed in @InterceptWith. When omitted but a tag is given, the base HttpClientInterceptor / HttpServerInterceptor type is used and the tag selects the instance. |
interceptorTag |
Interceptor tag class or array of tag classes for @InterceptWith(tag = ...). |
clientMapping |
Object with a type field. Replaces the generated per-status response mappers of a client method with @Mapping(type). Client mode only. |
An annotation value is written exactly as it appears in source, with a fully qualified type: @io.koraframework.resilient.retry.annotation.Retryable(MyRetry.class).
The leading @ is optional.
%{configPath} is replaced by the configuration path of the generated component:
- for a client, the
@HttpClientconfig path of that API (see Generated Client Usage) - for a server,
serverConfigPrefixwith%{ControllerTypeNameInCamelCase}replaced by the controller class name with a lower-case first letter (defaultserverConfigPrefixishttpServer.controller.%{ControllerTypeNameInCamelCase}, soPetApiControllergiveshttpServer.controller.petApiController)
configOptions = [
mode: "java-server",
extensions: """
{
"*": {
"additionalModelTypeAnnotations": "@java.lang.Deprecated",
"interceptorType": "io.koraframework.example.MyServerInterceptor"
},
"tags": {
"pet": {
"interceptorTag": ["io.koraframework.example.PetTag"]
}
},
"operations": {
"getPetById": {
"additionalMethodAnnotations": "@io.koraframework.example.Audited(\\"%{configPath}\\")"
}
}
}
"""
]
configOptions = mapOf(
"mode" to "kotlin-server",
"extensions" to """
{
"*": {
"additionalModelTypeAnnotations": "@java.lang.Deprecated",
"interceptorType": "io.koraframework.example.MyServerInterceptor"
},
"tags": {
"pet": {
"interceptorTag": ["io.koraframework.example.PetTag"]
}
},
"operations": {
"getPetById": {
"additionalMethodAnnotations": "@io.koraframework.example.Audited(\"%{configPath}\")"
}
}
}
"""
)
Generated server controllers are final unless enableServerValidation is enabled or additionalMethodAnnotations are configured;
adding a method annotation that relies on aspects automatically makes the controller non-final so the aspect can be woven.
Invalid JSON in extensions (or in tags) fails generation with a message showing the expected shape and the provided value.
Multiple Generation Tasks¶
Several GenerateTask tasks can be registered in one module, for example to generate two independent contracts,
or to generate a client for one contract and a server for another. Each task writes into the same outputDir and is added to the same source set,
so the only requirement is that generated packages do not collide. Give every task its own apiPackage/modelPackage/invokerPackage.
def openApiGeneratePetV2 = tasks.register("openApiGeneratePetV2", GenerateTask) {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/petstoreV2.yaml"
outputDir = "$buildDir/generated/openapi"
def corePackage = "io.koraframework.example.openapi.petV2" //(1)!
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
configOptions = [mode: "java-client", clientConfigPrefix: "httpClient.petV2"]
}
sourceSets.main { java.srcDirs += openApiGeneratePetV2.get().outputDir }
compileJava.dependsOn openApiGeneratePetV2
def openApiGeneratePetV3 = tasks.register("openApiGeneratePetV3", GenerateTask) {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/petstoreV3.yaml"
outputDir = "$buildDir/generated/openapi"
def corePackage = "io.koraframework.example.openapi.petV3" //(2)!
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
configOptions = [mode: "java-client", clientConfigPrefix: "httpClient.petV3"]
}
sourceSets.main { java.srcDirs += openApiGeneratePetV3.get().outputDir }
compileJava.dependsOn openApiGeneratePetV3
- Isolated package for the first contract
- Different package for the second contract, so class names cannot clash
val openApiGeneratePetV2 = tasks.register<GenerateTask>("openApiGeneratePetV2") {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/petstoreV2.yaml"
outputDir = "$buildDir/generated/openapi/petV2"
val corePackage = "io.koraframework.example.openapi.petV2" //(1)!
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
configOptions = mapOf("mode" to "kotlin-client", "clientConfigPrefix" to "httpClient.petV2")
}
val openApiGeneratePetV3 = tasks.register<GenerateTask>("openApiGeneratePetV3") {
generatorName = "kora"
inputSpec = "$projectDir/src/main/resources/openapi/petstoreV3.yaml"
outputDir = "$buildDir/generated/openapi/petV3"
val corePackage = "io.koraframework.example.openapi.petV3" //(2)!
apiPackage = "${corePackage}.api"
modelPackage = "${corePackage}.model"
invokerPackage = "${corePackage}.invoker"
configOptions = mapOf("mode" to "kotlin-client", "clientConfigPrefix" to "httpClient.petV3")
}
kotlin.sourceSets.main {
kotlin.srcDir(openApiGeneratePetV2.get().outputDir)
kotlin.srcDir(openApiGeneratePetV3.get().outputDir)
}
tasks.matching { it.name.startsWith("ksp") }.configureEach { //(3)!
dependsOn(openApiGeneratePetV2, openApiGeneratePetV3)
}
tasks.compileKotlin { dependsOn(openApiGeneratePetV2, openApiGeneratePetV3) }
- Isolated package for the first contract
- Different package for the second contract, so class names cannot clash
- Both
KSPandKotlincompilation must run after generation
Client¶
A minimal plugin configuration for creating a declarative HTTP client:
For clients, configOptions.mode is java-client.
Other client parameters are described below in the authorization, interceptors, tags, models, and implicit headers sections.
def openApiGenerateHttpClient = tasks.register("openApiGenerateHttpClient", GenerateTask) {
generatorName = "kora"
group = "openapi tools"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml" //(1)!
outputDir = "$buildDir/generated/openapi" //(2)!
def corePackage = "io.koraframework.example.openapi"
apiPackage = "${corePackage}.api" //(3)!
modelPackage = "${corePackage}.model" //(4)!
invokerPackage = "${corePackage}.invoker" //(5)!
openapiNormalizer = [
DISABLE_ALL: "true"
]
configOptions = [
mode: "java-client", //(6)!
clientConfigPrefix: "httpClient.myclient" //(7)!
]
}
sourceSets.main { java.srcDirs += openApiGenerateHttpClient.get().outputDir } //(8)!
compileJava.dependsOn openApiGenerateHttpClient //(9)!
- Path to the
OpenAPIfile used to create classes - Directory where generated files are created
- Package for delegates, controllers, and mappers
- Package for models and DTOs
- Auxiliary generator package
- Plugin mode
- Client configuration path prefix
- Register generated classes as project source code
- Make code compilation depend on HTTP client class generation: generate first, compile after
For clients, configOptions.mode is kotlin-client.
Other client parameters are described below in the authorization, interceptors, tags, models, and implicit headers sections.
val openApiGenerateHttpClient = tasks.register<GenerateTask>("openApiGenerateHttpClient") {
generatorName = "kora"
group = "openapi tools"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml" //(1)!
outputDir = "$buildDir/generated/openapi" //(2)!
val corePackage = "io.koraframework.example.openapi"
apiPackage = "${corePackage}.api" //(3)!
modelPackage = "${corePackage}.model" //(4)!
invokerPackage = "${corePackage}.invoker" //(5)!
openapiNormalizer = mapOf(
"DISABLE_ALL" to "true"
)
configOptions = mapOf(
"mode" to "kotlin-client", //(6)!
"clientConfigPrefix" to "httpClient.myclient" //(7)!
)
}
kotlin.sourceSets.main { kotlin.srcDir(openApiGenerateHttpClient.get().outputDir) } //(8)!
tasks.matching { it.name.startsWith("ksp") }.configureEach { dependsOn(openApiGenerateHttpClient) } //(9)!
tasks.compileKotlin { dependsOn(openApiGenerateHttpClient) }
- Path to the
OpenAPIfile used to create classes - Directory where generated files are created
- Package for delegates, controllers, and mappers
- Package for models and DTOs
- Auxiliary generator package
- Plugin mode
- Client configuration path prefix
- Register generated classes as project source code
- Make code compilation depend on HTTP client class generation: generate first, compile after
After generation, the HTTP client is available for dependency injection through the generated interface.
Client generation always needs a configuration path, so exactly one of these two options is required:
| Parameter | Description |
|---|---|
clientConfig |
Complete configuration path used verbatim for every generated client of the task (not specified by default, optional). Use it when the contract produces a single API interface. |
clientConfigPrefix |
Prefix to which the generated interface name with a lower-case first letter is appended (not specified by default, optional). Use it when the contract produces several API classes. |
If neither is set for a client mode, generation fails with a message suggesting a clientConfig value derived from the contract file name.
Generated Client Usage¶
For every API tag, the generator produces an interface annotated with @HttpClient, named after the tag (for example PetApi).
It is injected into components like any other Kora client, without extra registration:
With clientConfigPrefix, the configuration path is the prefix followed by the generated interface name with a lower-case first letter.
For clientConfigPrefix = "httpClient.petV2" and interface PetApi, the configuration block is httpClient.petV2.petApi.
With clientConfig, the value is used exactly as written and the interface name is not appended.
After a successful run the generator logs every generated client together with its configuration path, which is the quickest way to check the exact key.
The full set of client options (url, requestTimeout, per-operation blocks, telemetry) is described in the HTTP client documentation:
httpClient.petV2.petApi {
url = "https://localhost:8443" //(1)!
requestTimeout = "10s" //(2)!
getValuesConfig { //(3)!
requestTimeout = "20s"
}
telemetry.logging.enabled = true
}
- Base URL of the target service
- Default request timeout for all operations
- Per-operation override block, named after the
operationId(heregetValues)
httpClient:
petV2:
petApi:
url: "https://localhost:8443" #(1)!
requestTimeout: "10s" #(2)!
getValuesConfig: #(3)!
requestTimeout: "20s"
telemetry:
logging:
enabled: true
- Base URL of the target service
- Default request timeout for all operations
- Per-operation override block, named after the
operationId(heregetValues)
Every client method returns the *ApiResponses envelope of that operation, so the outcome is matched on the response subtype:
Optional Arguments¶
When an operation has optional query, header or cookie parameters, listing them all on every call is noisy.
Besides the full method, the generator produces a mutable holder class named <Api><OperationId>OptArgs and two extra default overloads:
one that takes only the required parameters, and one that takes the required parameters plus the holder.
var onlyRequired = petsApi.listPets(); //(1)!
var withOptional = petsApi.listPets(PetsApiListPetsOptArgs.defaults() //(2)!
.withLimit(50)); //(3)!
- Every optional parameter is passed as
null defaults()starts from the contract defaults,empty()starts from allnullwith...mutates the holder and returns it, so calls can be chained
val onlyRequired = petsApi.listPets() //(1)!
val withOptional = petsApi.listPets(PetsApiListPetsOptArgs.defaults() //(2)!
.withLimit(50)) //(3)!
- Every optional parameter is passed as
null defaults()starts from the contract defaults,empty()starts from allnullwith...mutates the holder and returns it, so calls can be chained
Client Authorization¶
If the OpenAPI contract describes securitySchemes, the generator creates an ApiSecurity module in apiPackage with:
- one marker class per security scheme, named after the scheme in
components.securitySchemeswith an upper-case first letter (apiKeyAuthbecomesApiSecurity.ApiKeyAuth,bearerAuthbecomesApiSecurity.BearerAuth) - one marker class and one
@DefaultComponentHttpClientInterceptorper distinct security requirement used by the operations - a
SecurityConfigrecord with@DefaultComponentconfiguration readers for theapiKeyandbasicschemes @InterceptWith(value = HttpClientInterceptor.class, tag = ApiSecurity.<Requirement>.class)on every secured client method
A security requirement that lists several schemes at once produces one combined marker joined with And (ApiSecurity.Sec1AndSec2),
and an operation that accepts several alternative requirements produces one marker joined with _ (ApiSecurity.BearerAuth_ApiKeyAuth).
The generated interceptor tries the alternatives in order and uses the first one for which every scheme provided a non-null token;
if none did, the request is sent unauthorized and a warning is logged, unless the contract also allows anonymous access.
securityConfigPrefix sets the configuration prefix of the generated SecurityConfig.
When it is not set, the prefix falls back to clientConfigPrefix + ".security", then to clientConfig + ".security", and finally to security.
| Parameter | Description |
|---|---|
securityConfigPrefix |
Configuration prefix for the generated SecurityConfig (not specified by default, optional). See the fallbacks above. |
authAsMethodArgument |
Passes the credential as a client method argument instead of generating interceptors (default: false). The whole ApiSecurity module is then not generated. |
primaryAuth |
Name of the security scheme turned into a method argument when an operation declares several (not specified by default, optional). Only meaningful with authAsMethodArgument. |
useSecurityDeclarationOrder |
Keeps the declaration order of schemes inside a security requirement (default: false). By default schemes are ordered alphabetically, so {a, b} and {b, a} share one interceptor. |
apiKey and basic¶
For apiKey and basic schemes, the generator produces @DefaultComponent config readers and token providers, so no beans are required — only configuration values.
An apiKey scheme reads a single string; a basic scheme reads a username/password object.
Both values are optional: when they are absent the scheme simply provides no token.
openapiAuth {
apiKeyAuth = "MyAuthApiKey" //(1)!
basicAuth { //(2)!
username = "user"
password = "password"
}
}
apiKeyschemeapiKeyAuth: value sent in the header, query parameter or cookie declared by the schemebasicschemebasicAuth: credentials wrapped by the generatedBasicAuthHttpClientTokenProvider
The path above corresponds to securityConfigPrefix = "openapiAuth":
bearer and oauth¶
For bearer, oauth2 and openId schemes the generator does not know where the token comes from, so it expects an
HttpClientTokenProvider component tagged with the generated marker class for that scheme.
The returned value is sent as the whole Authorization header, so it must include the Bearer prefix when the scheme requires it:
@Module
public interface ClientAuthModule {
@Tag(ApiSecurity.BearerAuth.class) //(1)!
default HttpClientTokenProvider bearerTokenProvider() {
return request -> "Bearer my-token"; //(2)!
}
}
- Tag must match the generated marker class for the scheme
- Real implementations usually fetch or refresh the token here, and return
nullwhen no token is available
@Module
interface ClientAuthModule {
@Tag(ApiSecurity.BearerAuth::class) //(1)!
fun bearerTokenProvider(): HttpClientTokenProvider {
return HttpClientTokenProvider { "Bearer my-token" } //(2)!
}
}
- Tag must match the generated marker class for the scheme
- Real implementations usually fetch or refresh the token here, and return
nullwhen no token is available
Every scheme needs a provider
The generated ApiSecurity module requires an HttpClientTokenProvider for every bearer, oauth2 or openId scheme
declared in components.securitySchemes, even for schemes the application never uses — otherwise the graph fails to build.
Such an unused scheme must return null, because the interceptor applies the first requirement whose providers all returned a token
and a stray non-null value would override the scheme you actually wanted.
Multiple schemes¶
When an operation declares several alternative security requirements, the generator builds one interceptor covering all of them and applies the first requirement whose schemes all returned a token — no option is needed for this.
To pass the credentials explicitly per call instead of through an interceptor, enable authAsMethodArgument.
The authorization value then becomes a @Nullable String client method argument annotated with @Header, @Query or @Cookie according to the scheme,
and ApiSecurity is not generated at all. primaryAuth picks which scheme becomes that argument when an operation lists several:
configOptions = [
mode: "java-client",
clientConfigPrefix: "httpClient.petV3",
authAsMethodArgument: "true", //(1)!
primaryAuth: "apiKeyAuth" //(2)!
]
- Add the auth value as a method argument instead of generating interceptors
- Scheme turned into the argument when an operation lists several
configOptions = mapOf(
"mode" to "kotlin-client",
"clientConfigPrefix" to "httpClient.petV3",
"authAsMethodArgument" to "true", //(1)!
"primaryAuth" to "apiKeyAuth" //(2)!
)
- Add the auth value as a method argument instead of generating interceptors
- Scheme turned into the argument when an operation lists several
If the selected scheme maps to the Authorization header but the operation already declares an explicit Authorization header parameter,
generation fails with a message asking to rename that parameter or to disable authAsMethodArgument.
Additional Annotations¶
extensions.additionalMethodAnnotations adds annotations above generated client or server controller methods.
It is set globally under *, per contract tag under tags, or per operationId under operations, and the three levels are combined.
configOptions = mapOf(
"mode" to "kotlin-client",
"clientConfigPrefix" to "httpClient.petV3",
"extensions" to """
{
"*": {
"additionalMethodAnnotations": "@io.koraframework.example.CommonAnnotation"
},
"tags": {
"pet": {
"additionalMethodAnnotations": ["@io.koraframework.example.PetAnnotation"]
}
}
}
"""
)
Model and enum annotations use additionalModelTypeAnnotations, additionalEnumTypeAnnotations, or additionalTypeAnnotations for both,
and are only read from the * section because a generated model is not bound to a single operation.
Interceptors¶
Generated client methods can be annotated with interceptors through extensions.
interceptorType sets the implementation class and interceptorTag sets the tags. Both may be used together, or only one of them:
- only
interceptorType—@InterceptWith(MyInterceptor.class) - only
interceptorTag—@InterceptWith(value = HttpClientInterceptor.class, tag = MyTag.class), so the instance is picked from the graph by tag - both —
@InterceptWith(value = MyInterceptor.class, tag = MyTag.class)
interceptorTag accepts a single class name or an array of class names; an array produces one @InterceptWith per tag.
configOptions = [
mode: "java-client",
clientConfigPrefix: "httpClient.petV3",
extensions: """
{
"*": {
"interceptorTag": "io.koraframework.example.MyTag"
},
"tags": {
"pet": {
"interceptorType": "io.koraframework.example.MyInterceptor"
},
"shop": {
"interceptorType": "io.koraframework.example.MyInterceptor",
"interceptorTag": ["io.koraframework.example.MyTag"]
}
}
}
"""
]
configOptions = mapOf(
"mode" to "kotlin-client",
"clientConfigPrefix" to "httpClient.petV3",
"extensions" to """
{
"*": {
"interceptorTag": "io.koraframework.example.MyTag"
},
"tags": {
"pet": {
"interceptorType": "io.koraframework.example.MyInterceptor"
},
"shop": {
"interceptorType": "io.koraframework.example.MyInterceptor",
"interceptorTag": ["io.koraframework.example.MyTag"]
}
}
}
"""
)
Tags¶
Generated clients annotated with @HttpClient can receive httpClientTag and telemetryTag parameters.
The value is a JSON object where the key is an API tag from the contract, or * for all of them, and the value is an object with httpClientTag and telemetryTag fields.
Set configOptions.tags:
Implicit Headers¶
By default, headers from an OpenAPI operation become generated method arguments.
If some headers are supplied by infrastructure rather than application code, they can be made implicit.
implicitHeaders = truemakes all headers fromOpenAPIoperations implicit.implicitHeadersRegexmakes only headers whose names match the regular expression implicit.
An implicit header is removed from the method signature but remains in OpenAPI annotations in generated code
(@io.swagger.v3.oas.annotations.Parameter(in = ParameterIn.HEADER)).
This keeps the header in contract documentation without requiring application code to pass it manually.
Models¶
The generator creates request and response models from OpenAPI schemas.
Java models are record types annotated with @Json writers and readers; Kotlin models are data class types.
Schemas with a discriminator produce a sealed interface with the mapped models as permitted subtypes.
Java records also get a with<Field> method per field that returns a new instance, or the same instance when the value did not change:
Use named arguments in Kotlin
Generated Kotlin constructors list required properties first and give every optional property a default value.
Adding a property to the contract can therefore shift positions, so construct models with named arguments:
Pet(id = 1L, name = "name", status = Pet.StatusEnum.AVAILABLE).
Enums¶
A schema enum becomes a generated enum whose constants keep the raw contract values in a nested Constants class.
Because contract values are frequently not valid identifiers (Dingo-Don, 5), the enum is converted from its wire value with the static fromValue method,
and getValue() returns the wire value back. Enum.valueOf works on the generated constant name, not on the contract value, and must not be used for parsing:
- Throws
IllegalArgumentExceptionfor a value that is not in the contract - Returns
"available", the value declared in the contract
For every generated enum the generator also emits a @Module with @DefaultComponent JsonReader, JsonWriter and HTTP parameter converters,
so enums work as request bodies, query parameters, path parameters and headers without any manual mapper.
Optional Nullable Fields¶
A field that is both nullable: true and absent from the required list has three distinguishable states:
the field is absent from JSON, the field is present with null, and the field is present with a value.
Such a field is generated as JsonNullable so the three states remain distinguishable:
falsewhen the field was absent from the request body- May still be
nullwhen the field was explicitly sent asnull
The other combinations are simpler:
requiredand notnullable— a plain non-null field- not
requiredand notnullable— a@Nullablefield inJava, aT?field inKotlin requiredandnullable— a@Nullable/T?field annotated with@JsonInclude(ALWAYS), sonullis always serialized
Model Filtering¶
OpenAPI Generator can filter operations through openapiNormalizer.FILTER.
If filterWithModels is additionally enabled, the Kora generator also excludes the models that became unused after operation filtering.
This is useful for large contracts where an application generates only part of the API.
Responses¶
For every operation the generator produces a <Api>Responses interface containing one response type per operation.
When an operation declares several responses, that type is a sealed interface with one record / data class per declared status code,
named <OperationId><Code>ApiResponse. A response with a body carries it as content; declared response headers become extra components.
When an operation declares exactly one response, <OperationId>ApiResponse is that record directly, without a sealed wrapper.
Status ranges (1XX, 2XX, 3XX, 4XX, 5XX) and the default response cannot be represented as a fixed int,
so their records carry a runtime int statusCode as the first component:
var response = petsApi.listPets();
return switch (response) {
case PetsApiResponses.ListPetsApiResponse.ListPets200ApiResponse r -> r.content();
case PetsApiResponses.ListPetsApiResponse.ListPets4XXApiResponse r -> throw new IllegalStateException("Client error " + r.statusCode()); //(1)!
case PetsApiResponses.ListPetsApiResponse.ListPets5XXApiResponse r -> throw new IllegalStateException("Server error " + r.statusCode());
};
- The real status code, because
4XXcovers a whole range
val response = petsApi.listPets()
return when (response) {
is PetsApiResponses.ListPetsApiResponse.ListPets200ApiResponse -> response.content
is PetsApiResponses.ListPetsApiResponse.ListPets4XXApiResponse -> throw IllegalStateException("Client error " + response.statusCode) //(1)!
is PetsApiResponses.ListPetsApiResponse.ListPets5XXApiResponse -> throw IllegalStateException("Server error " + response.statusCode)
}
- The real status code, because
4XXcovers a whole range
On the client side exact codes are registered with @ResponseCodeMapper(code = N), while ranges and default are funnelled through one
@ResponseCodeMapper(code = ResponseCodeMapper.DEFAULT) mapper that dispatches on the real status code.
If the contract declares no default response and the received status matches nothing, the client throws HttpClientResponseException.
For a client, when several responses of one operation share the same body type, the generator additionally emits a shared sealed interface
<OperationId><Type>ApiResponse exposing content() and statusCode(), so all error variants of one model can be handled in one branch.
Server¶
A minimal plugin configuration for creating HTTP server handlers:
For servers, configOptions.mode is java-server.
Other server parameters are described below in the validation, delegate classes, interceptors, models, and implicit headers sections.
def openApiGenerateHttpServer = tasks.register("openApiGenerateHttpServer", GenerateTask) {
generatorName = "kora"
group = "openapi tools"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml" //(1)!
outputDir = "$buildDir/generated/openapi" //(2)!
def corePackage = "io.koraframework.example.openapi"
apiPackage = "${corePackage}.api" //(3)!
modelPackage = "${corePackage}.model" //(4)!
invokerPackage = "${corePackage}.invoker" //(5)!
openapiNormalizer = [
DISABLE_ALL: "true"
]
configOptions = [
mode: "java-server", //(6)!
]
}
sourceSets.main { java.srcDirs += openApiGenerateHttpServer.get().outputDir } //(7)!
compileJava.dependsOn openApiGenerateHttpServer //(8)!
- Path to the
OpenAPIfile used to create classes - Directory where generated files are created
- Package for delegates, controllers, and mappers
- Package for models and DTOs
- Auxiliary generator package
- Plugin mode
- Register generated classes as project source code
- Make code compilation depend on HTTP server class generation: generate first, compile after
For servers, configOptions.mode is kotlin-server.
Other server parameters are described below in the validation, delegate classes, interceptors, models, and implicit headers sections.
val openApiGenerateHttpServer = tasks.register<GenerateTask>("openApiGenerateHttpServer") {
generatorName = "kora"
group = "openapi tools"
inputSpec = "$projectDir/src/main/resources/openapi/openapi.yaml" //(1)!
outputDir = "$buildDir/generated/openapi" //(2)!
val corePackage = "io.koraframework.example.openapi"
apiPackage = "${corePackage}.api" //(3)!
modelPackage = "${corePackage}.model" //(4)!
invokerPackage = "${corePackage}.invoker" //(5)!
openapiNormalizer = mapOf(
"DISABLE_ALL" to "true"
)
configOptions = mapOf(
"mode" to "kotlin-server" //(6)!
)
}
kotlin.sourceSets.main { kotlin.srcDir(openApiGenerateHttpServer.get().outputDir) } //(7)!
tasks.matching { it.name.startsWith("ksp") }.configureEach { dependsOn(openApiGenerateHttpServer) } //(8)!
tasks.compileKotlin { dependsOn(openApiGenerateHttpServer) }
- Path to the
OpenAPIfile used to create classes - Directory where generated files are created
- Package for delegates, controllers, and mappers
- Package for models and DTOs
- Auxiliary generator package
- Plugin mode
- Register generated classes as project source code
- Make code compilation depend on HTTP server class generation: generate first, compile after
For every API tag, the generator produces a <Api>Controller annotated with @Component and @HttpController, so handlers are registered automatically.
The controller only unpacks the request and delegates to the <Api>Delegate contract that the application implements.
Validation¶
To generate models and controllers with annotations from the validation module, set enableServerValidation:
When enableServerValidation is enabled, the generator marks models with @Valid, translates the schema constraints into Kora validation annotations,
and adds @Validate to controller methods with validated parameters.
minimum/maximum become @Min, @Max or @Range(from, to, boundary) depending on how many bounds the schema declares;
minLength/maxLength and minItems/maxItems become @Size; pattern becomes @Pattern.
enableServerValidationInterceptor controls adding @InterceptWith(ValidationHttpServerInterceptor.class), which converts validation errors to HTTP responses.
It defaults to enabled whenever server validation is enabled.
Setting enableServerValidationInterceptor = "false" keeps the validation annotations but does not add the standard interceptor,
which is what you want when ViolationException is mapped by your own response mapper.
Both options are read only in server modes.
Delegate Implementation¶
The server generator creates a controller and a delegate contract where the user implements application logic.
By default, delegateMethodBodyMode = none, so delegate contract methods are abstract and must be implemented by the application.
If delegateMethodBodyMode = throwException is set, methods become default and throw UnsupportedOperationException("Not yet implemented"),
and the generator additionally creates a <Api>Module module with a default delegate implementation.
This mode is useful when the application must be built before all operations are implemented, or when custom implementations are connected gradually.
Add the generated <Api>Module to the application graph so the default implementation is available:
Delegate Response Types¶
Each generated delegate method returns the sealed <Api>Responses envelope of the operation, described in Responses.
For an operation getPetById with responses 200 and 404, the generator produces PetApiResponses.GetPetByIdApiResponse with subtypes
GetPetById200ApiResponse (carrying the body) and GetPetById404ApiResponse. The implementation returns the subtype matching the outcome:
@Component
public final class PetDelegate implements PetApiDelegate {
private final Map<Long, Pet> petMap = new ConcurrentHashMap<>();
@Override
public PetApiResponses.GetPetByIdApiResponse getPetById(long petId) {
var pet = petMap.get(petId);
if (pet == null) {
return new PetApiResponses.GetPetByIdApiResponse.GetPetById404ApiResponse(); //(1)!
}
return new PetApiResponses.GetPetByIdApiResponse.GetPetById200ApiResponse(pet); //(2)!
}
@Override
public PetApiResponses.AddPetApiResponse addPet(Pet body) {
petMap.put(body.id(), body);
return new PetApiResponses.AddPetApiResponse.AddPet200ApiResponse(body);
}
}
- Status
404subtype, no body - Status
200subtype carrying the response body
@Component
class PetDelegate : PetApiDelegate {
private val petMap = ConcurrentHashMap<Long, Pet>()
override fun getPetById(petId: Long): PetApiResponses.GetPetByIdApiResponse {
val pet = petMap[petId]
return if (pet == null) {
PetApiResponses.GetPetByIdApiResponse.GetPetById404ApiResponse() //(1)!
} else {
PetApiResponses.GetPetByIdApiResponse.GetPetById200ApiResponse(pet) //(2)!
}
}
override fun addPet(pet: Pet): PetApiResponses.AddPetApiResponse {
petMap[pet.id] = pet
return PetApiResponses.AddPetApiResponse.AddPet200ApiResponse(pet)
}
}
- Status
404subtype, no body - Status
200subtype carrying the response body
Java delegate methods declare throws Exception, so an implementation may propagate checked exceptions
and let an interceptor or an exception response mapper turn them into a response.
Raw Request in Delegate¶
By default, a delegate method receives only the parameters declared in the contract. If an implementation needs access to the raw request
(for example to read an infrastructure header or the remote address), enable requestInDelegateParams. The generator then adds an
HttpServerRequest as the first parameter of every delegate method. This is a server-only option.
- Adds
HttpServerRequest _serverRequestas the first argument of each delegate method
Controller Path Prefix¶
prefixPath prepends a base path to every generated HTTP server controller route. It is useful when all operations should be served under a common
segment (for example /api/v1) that is not part of the OpenAPI paths.
Interceptors¶
Generated controllers annotated with @HttpController can also be annotated with interceptors through extensions,
using exactly the same fields as client interceptors.
When only interceptorTag is given, the base type is HttpServerInterceptor and the instance is resolved from the graph by tag:
configOptions = [
mode: "java-server",
extensions: """
{
"*": {
"interceptorTag": "io.koraframework.example.MyTag"
},
"tags": {
"pet": {
"interceptorType": "io.koraframework.example.MyInterceptor"
},
"shop": {
"interceptorType": "io.koraframework.example.MyInterceptor",
"interceptorTag": ["io.koraframework.example.MyTag"]
}
}
}
"""
]
configOptions = mapOf(
"mode" to "kotlin-server",
"extensions" to """
{
"*": {
"interceptorTag": "io.koraframework.example.MyTag"
},
"tags": {
"pet": {
"interceptorType": "io.koraframework.example.MyInterceptor"
},
"shop": {
"interceptorType": "io.koraframework.example.MyInterceptor",
"interceptorTag": ["io.koraframework.example.MyTag"]
}
}
}
"""
)
An interceptor referenced by interceptorType must be a component of the graph, so declare it with @Component or as a module method.
Authorization¶
When the OpenAPI contract describes securitySchemes, the server generator creates an ApiSecurity module with one marker class per scheme,
named after the scheme name in components.securitySchemes with an upper-case first letter — for the usual
Basic/ApiKey/Bearer/OAuth contract these are
ApiSecurity.BasicAuth, ApiSecurity.ApiKeyAuth, ApiSecurity.BearerAuth and ApiSecurity.OAuth.
For each scheme, the application must provide an HttpServerPrincipalExtractor<T, P> component tagged with the matching marker class.
T is the extracted credential and P is the resulting principal.
The extractor receives the request and the credential value, and returns the authenticated principal or null when the credential is not accepted:
@Module
public interface AuthModule {
@Tag(ApiSecurity.BearerAuth.class)
default HttpServerPrincipalExtractor<String, Principal> bearerHttpServerPrincipalExtractor() {
return (request, value) -> new UserPrincipal("name"); //(1)!
}
@Tag(ApiSecurity.BasicAuth.class)
default HttpServerPrincipalExtractor<String, Principal> basicHttpServerPrincipalExtractor() {
return (request, value) -> new UserPrincipal("name");
}
@Tag(ApiSecurity.ApiKeyAuth.class)
default HttpServerPrincipalExtractor<String, Principal> apiKeyHttpServerPrincipalExtractor() {
return (request, value) -> new UserPrincipal("name");
}
@Tag(ApiSecurity.OAuth.class)
default HttpServerPrincipalExtractor<String, PrincipalWithScopes> oauthHttpServerPrincipalExtractor() { //(2)!
return (request, value) -> new UserPrincipal("name");
}
}
- Returning
nullrejects this security requirement, and the generated interceptor tries the next one OAuthschemes declare scopes, so the extractor returns aPrincipalWithScopes
@Module
interface AuthModule {
@Tag(ApiSecurity.BearerAuth::class)
fun bearerHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor<String, Principal> {
return HttpServerPrincipalExtractor { _, _ -> UserPrincipal("name") } //(1)!
}
@Tag(ApiSecurity.BasicAuth::class)
fun basicHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor<String, Principal> {
return HttpServerPrincipalExtractor { _, _ -> UserPrincipal("name") }
}
@Tag(ApiSecurity.ApiKeyAuth::class)
fun apiKeyHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor<String, Principal> {
return HttpServerPrincipalExtractor { _, _ -> UserPrincipal("name") }
}
@Tag(ApiSecurity.OAuth::class)
fun oauthHttpServerPrincipalExtractor(): HttpServerPrincipalExtractor<String, PrincipalWithScopes> { //(2)!
return HttpServerPrincipalExtractor { _, _ -> UserPrincipal("name") }
}
}
- Returning
nullrejects this security requirement, and the generated interceptor tries the next one OAuthschemes declare scopes, so the extractor returns aPrincipalWithScopes
For OAuth, the returned principal must implement PrincipalWithScopes so the generated interceptor can enforce the scopes declared on each operation.
The authenticated principal is published for the whole request through Principal.current():
An operation that requires several schemes at once gets one extractor whose credential type is a generated <Tag>AuthData record
holding one String per scheme, and whose tag joins the scheme names with With — for schemes headerAuth1 and queryAuth this is
@Tag(ApiSecurity.HeaderAuth1WithQueryAuth.class) and ApiSecurity.HeaderAuth1WithQueryAuthAuthData.
When no security requirement of an operation is satisfied, the generated interceptor throws HttpServerResponseException.of(401, "Unauthorized").
If the contract lists an empty requirement (security: [{}]) as one of the alternatives, the request is passed through unauthenticated instead.
Server security supports apiKey schemes in a header, a query parameter or a cookie, and http basic/bearer plus oauth2/openId
schemes read from the Authorization header. Any other scheme type fails generation with an explicit message.
Recommendations¶
Advice
If something is not generated by the plugin, or behavior differs from expectations or from other versions, carefully check the plugin configuration and study the settings, because they can affect how classes are generated.
Starting with plugin version 7.0.0, the SIMPLIFY_ONEOF_ANYOF rule enabled by default in openapiNormalizer
can lead to some non-obvious generator results, so contracts with oneOf/anyOf are usually generated with DISABLE_ALL: "true".
If a generated client cannot find its configuration, check the log line the generator prints after a successful run: it lists every generated client together with the exact configuration path it expects.
The generator runs on the Gradle JVM, so a UnsupportedClassVersionError during the generation task means the Gradle daemon
runs on an older JDK than the one Kora is compiled for — see Dependency.