OpenAPITools / OpenAPITools/openapi-generator

[Kotlin] Improve handling of multiple response content types and allow selecting preferred return type

Open
#22,242 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Enhancement: Feature
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:

  1. The order of keys in a YAML/JSON map (like the content map) should not have semantic significance according to the OpenAPI specification. Relying on this order makes the code generation fragile.
  2. It forces users into a brittle workaround: manually ordering the content keys 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.
  3. 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/json which maps to SchemaV1_1 over application/vnd.app.v1.0+json which maps to SchemaV1_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 UserApi interface.
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.Factory logic.

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

  1. Spec Reordering (Current Workaround): Manually editing the openapi.yaml to ensure the desired content type (e.g., application/json) is the first key listed in the content map. This works but is brittle, non-standard, and requires comments in the spec to prevent accidental breakage.
  2. 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.
  3. Custom Retrofit Converter: While a custom Converter.Factory can be injected and can parse different content types at runtime, it cannot fix the generated interface signature. If the signature expects Response<UserProfileV10>, the custom converter must ultimately return UserProfileV10, potentially discarding data parsed from a UserProfileV11 payload. 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/json representing the latest backward-compatible version. Need the generated client code to default to the latest version's schema.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.