OpenAPITools / OpenAPITools/openapi-generator
[Kotlin] Improve handling of multiple response content types and allow selecting preferred return type
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 26.8k
- Forks
- 7.7k
- PR merge metrics
- PR metrics pending
Description
Is your feature request related to a problem? Please describe.
When an OpenAPI operation defines multiple content types for a successful response (e.g., a 200 OK or 201 Created), each potentially referencing a different schema, the current Kotlin generator (observed with jvm-retrofit2 library, likely affects others) determines the single return type for the generated API interface method based on the schema associated with the first content type listed in the content map within the spec file.
This behavior is problematic because:
- The order of keys in a YAML/JSON map (like the
contentmap) should not have semantic significance according to the OpenAPI specification. Relying on this order makes the code generation fragile. - It forces users into a brittle workaround: manually ordering the
contentkeys in the spec file to ensure the generator picks the desired schema for the interface signature. This workaround is prone to being accidentally broken during spec maintenance, reformatting, or by tools that alphabetize keys. - It doesn't allow the user to explicitly declare which response type should be considered the "default" or "preferred" type for the generated static interface, especially when dealing with versioned content types where a later, backward-compatible version is desired (e.g., preferring
application/jsonwhich maps toSchemaV1_1overapplication/vnd.app.v1.0+jsonwhich maps toSchemaV1_0).
Example Scenario:
An endpoint defines:
responses:
'200':
content:
application/vnd.myapi.v1.0+json:
schema:
$ref: '#/components/schemas/UserProfileV10'
application/json: # Logically the default/latest
schema:
$ref: '#/components/schemas/UserProfileV11' # Backward compatible
The generator currently produces a Retrofit method returning UserProfileV10. The desired outcome is a method returning UserProfileV11.
Describe the solution you'd like
The generator should provide a more robust and predictable way to determine the primary return type. I see a few potential solutions:
Solution 1: Configuration Option (Preferred)
Introduce a new configuration option to allow users to override the default "pick-the-first" logic. This could be done in two ways:
A) Simple (Global)
Introduce a single option, preferredResponseContentType. This would apply to all endpoints.
- Pro: Very simple to configure.
- Con: It's a "blunt instrument" and not flexible if different endpoints have different preferred types.
B) Advanced/Ideal (Per-Operation Map)
Introduce an option that accepts a map of operationId to preferredContentType.
For example, a config might look like:
openApiGenerate {
// ... other config ...
configOptions.set(
mapOf(
"library" to "jvm-retrofit2",
"packageName" to "demo.api.client.gen.openapi",
// ... other options ...
// --- Proposed New Option ---
"preferredResponseContentTypeMap" to mapOf(
"getV1CurrentUser" to "application/json",
"createV1User" to "application/json",
"getV1UserById" to "application/json",
"updateV1CurrentUser" to "application/json",
"updateV1User" to "application/json"
)
)
)
}
- Pro: This is extremely flexible and precise, solving the "blunt instrument" problem.
- Pro: It keeps the generated interface clean (one method per operation).
- Pro: It's ideal for backward-compatible models where the user just wants to ensure the generator picks the "best" model for each specific endpoint.
- Con: Requires slightly more configuration from the user (but is worth the effort).
Solution 2: Generate Multiple Methods (Preferred Robust Solution)
A better, more flexible solution would be to enhance the generator to create a separate function for each defined content type, adding the appropriate @Headers("Accept: ...") annotation to each one.
For example:
interface UserApi {
/**
* Retrieves v1.0 schema
*/
@Headers("Accept: application/vnd.myapi.v1.0+json")
@GET("v1/user/me")
suspend fun getV1CurrentUserV10(): Response<UserProfileV10>
/**
* Retrieves v1.1 schema
*/
@Headers("Accept: application/vnd.myapi.v1.1+json")
@GET("v1/user/me")
suspend fun getV1CurrentUserV11(): Response<UserProfileV11>
/**
* Retrieves default (latest, v1.1) schema
*/
@Headers("Accept: application/json")
@GET("v1/user/me")
suspend fun getV1CurrentUserDefault(): Response<UserProfileV11>
}
- Pro: This most accurately reflects the API's capabilities and gives the developer full, explicit control over which version to request.
- Con: This significantly clutters the generated
UserApiinterface.
Solution 3: Advanced Runtime Handling
While likely a much larger change and potentially breaking, a more advanced solution could mimic generators like Apple's swift-openapi-generator (https://github.com/apple/swift-openapi-generator).
This approach generates a single function that returns a sealed class wrapper. The generated implementation would then check the response's Content-Type header at runtime and deserialize the body into the correct model, returning the appropriate sealed class case.
- Pro: This is the most robust solution, as it correctly handles whatever the server sends.
- Con: This is a major architectural change that conflicts with Retrofit's standard converter model, which expects a single, known type in the signature. It would likely require generating complex custom
Converter.Factorylogic.
Given these options, Solution 1 (especially 1B) seems like the most immediately feasible, flexible, and non-breaking improvement for most users.
Describe alternatives you've considered
- Spec Reordering (Current Workaround): Manually editing the
openapi.yamlto ensure the desired content type (e.g.,application/json) is the first key listed in thecontentmap. This works but is brittle, non-standard, and requires comments in the spec to prevent accidental breakage. - Custom Templates: Attempting to override the template logic is extremely difficult due to Mustache limitations and reliance on the generator's pre-calculated
returnType. It doesn't seem like a sustainable approach. - Custom Retrofit Converter: While a custom
Converter.Factorycan be injected and can parse different content types at runtime, it cannot fix the generated interface signature. If the signature expectsResponse<UserProfileV10>, the custom converter must ultimately returnUserProfileV10, potentially discarding data parsed from aUserProfileV11payload. The generated signature itself needs to be correct, which the spec reordering workaround achieves, albeit fragilely.
Additional context
- Generator:
kotlin - Library:
jvm-retrofit2(Problem likely exists in other Kotlin libraries too) - Generator Version: [Specify the version you are using, e.g.,
7.9.0] - Use Case: API versioning using custom media types alongside a default
application/jsonrepresenting the latest backward-compatible version. Need the generated client code to default to the latest version's schema.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in the Kotlin generator's jvm-retrofit2 response return-type selection and content-map handling. Compare how the generator represents multiple response content types and review the existing configuration path before choosing between preferred-type configuration and generating separate methods. Done should include deterministic handling that does not depend on map order, with behavior covered for the example's multiple schemas.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- kotlin, openapi
- Domain
- api, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100