Skip to content

[kotlin][jvm-spring-restclient] honour nullableReturnType in body access#23613

Open
takkiraz wants to merge 3 commits intoOpenAPITools:masterfrom
takkiraz:fix/kotlin-spring-restclient-nullable-return
Open

[kotlin][jvm-spring-restclient] honour nullableReturnType in body access#23613
takkiraz wants to merge 3 commits intoOpenAPITools:masterfrom
takkiraz:fix/kotlin-spring-restclient-nullable-return

Conversation

@takkiraz
Copy link
Copy Markdown
Contributor

@takkiraz takkiraz commented Apr 24, 2026

Fixes #23612

Problem

When using the kotlin generator with library=jvm-spring-restclient and nullableReturnType=true, the generated API function signature correctly declares a nullable return type (e.g. String?), but the implementation force-unwraps the response body with !!. This causes a NullPointerException at runtime whenever the server returns an empty body, even though the signature promises that null is a valid return value.

Minimal reproducer:

paths:
  /ping:
    post:
      operationId: ping
      responses:
        "200":
          description: body may be empty
          content:
            text/html:
              schema:
                type: string

Generated with --additional-properties=nullableReturnType=true,useSpringBoot3=true,serializationLibrary=jackson:

fun ping(...): kotlin.String? {         // signature says null is valid
    val result = pingWithHttpInfo(...)
    return result.body!!                // but we crash on null anyway
}

The contract says "can return null", the implementation says "will crash on null". These are inconsistent.

Fix

Three template changes that together honour nullableReturnType:

  1. api.mustache — drop the !! when the return type is nullable:

    -        return result.body!!
    +        return result.body{{^nullableReturnType}}!!{{/nullableReturnType}}
  2. api.mustache — propagate nullability into the generic type of the WithHttpInfo variant for consistency:

    -        return request<..., {{{returnType}}}{{^returnType}}Unit{{/returnType}}>(
    +        return request<..., {{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}}>(
  3. infrastructure/ApiClient.kt.mustache — relax the generic bound from T: Any to T: Any? so that ResponseEntity<T?> compiles:

    -    protected inline fun <reified I : Any, reified T: Any> request(...)
    +    protected inline fun <reified I : Any?, reified T: Any?> request(...)

    (Plus the corresponding adjustments on prepare() and nullableBody() and a cast on the body value to keep RestClient.body() happy.)

When nullableReturnType=false (the default), the generated code is unchanged.

Test coverage

A new sample kotlin-jvm-spring-3-restclient-nullable-return has been added that exercises nullableReturnType=true. It includes a regression test (NullableReturnTypeTest) that uses Spring's MockServerRestClientCustomizer to assert:

  • an empty response body is returned as null (the case that previously threw NPE),
  • a non-empty response body is returned as-is.

Reverting either template change before running the sample's tests reproduces the original NPE, confirming the fix is exercised.

bin/utils/test_file_list.yaml and .github/workflows/samples-kotlin-client.yaml have been updated so the new sample builds and runs in CI.

Compatibility

Generated API consumers

  • With nullableReturnType=false (default) the generated API classes are identical; no source- or binary-level change visible.
  • With nullableReturnType=true the generated API classes now correctly handle null response bodies instead of throwing NPE. Any code that relied on the broken !! behaviour was already crashing in the empty-body edge case.

ApiClient generic bound relaxation

The ApiClient.request<I, T>() function is protected and inline. Its generic bound was relaxed from T : Any to T : Any?. This is a strictly widening type change:

  • Every type that satisfied T : Any (String, Pet, Unit, …) continues to satisfy T : Any? — all existing call-sites in generated code and samples compile unchanged.
  • Binary compatibility of inline functions is a non-issue because their bodies are inlined at each call site; there is no separate bytecode to replace.
  • The only theoretical breakage is for consumers who subclass a generated API class (or ApiClient directly) and override request() with an explicit T : Any bound. This is an exceptionally uncommon pattern since the function is an internal dispatch helper, not a public API boundary.

The same approach (relaxing generic bounds to allow T : Any?) is already used elsewhere in the Kotlin client templates where nullability matters, so this keeps the jvm-spring-restclient library consistent with the ecosystem.

cc @karismann @Zomzog @andrewemery @4brunu @yutaka0m @stefankoppier @e5l @dennisameling (Kotlin technical committee)

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work.
  • Ran ./mvnw clean package on the relevant modules and ./bin/generate-samples.sh for the affected configs. All touched samples compile; the new sample's tests pass.
  • File the PR against master (fix is additive with fallback via nullableReturnType being opt-in).
  • @mentioned the Kotlin technical committee above.

Summary by cubic

Fixes null handling for nullableReturnType=true in the kotlin jvm-spring-restclient generator by returning nullable bodies instead of force-unwrapping, preventing NPEs on empty responses. Also updates generated API docs and CI to reflect nullable return types.

  • Bug Fixes

    • In api.mustache, return result.body when nullableReturnType=true; keep !! otherwise.
    • In api.mustache WithHttpInfo, propagate ? to ResponseEntity generic (e.g., ResponseEntity<String?>).
    • In infrastructure/ApiClient.kt.mustache, relax generics to I: Any? and T: Any?, update prepare/nullableBody, and cast body to Any for RestClient.body().
    • In api_doc.mustache, add ? in the method signature header and “Return type” when nullableReturnType=true.
    • Added sample kotlin-jvm-spring-3-restclient-nullable-return with a regression test; added CI trigger paths so changes to this sample run in CI.
    • Regenerated affected samples and docs to pick up nullability and client changes.
  • Migration

    • No action needed; generated code is unchanged by default.

Written for commit ec8c734. Summary will update on new commits.

When `nullableReturnType=true` is set, the generated API function
signature correctly returns a nullable type (e.g. `String?`), but the
implementation force-unwraps the response body with `!!`. This throws
a NullPointerException at runtime whenever the server returns an empty
body, even though the signature promises that null is a valid return
value.

This change:

* Drops the `!!` in `api.mustache` when `nullableReturnType` is enabled.
* Propagates nullability into the generic type parameter of the
  `WithHttpInfo` variant for consistency.
* Relaxes the type parameter in `ApiClient.kt.mustache` from `T: Any`
  to `T: Any?` so that `ResponseEntity<T?>` compiles.
* Adds a new sample `kotlin-jvm-spring-3-restclient-nullable-return`
  that exercises `nullableReturnType=true`, including a regression
  test that reproduces the original NPE when the fix is reverted.
@takkiraz takkiraz force-pushed the fix/kotlin-spring-restclient-nullable-return branch from 2141bc4 to 3504c25 Compare April 24, 2026 11:30
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 31 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".github/workflows/samples-kotlin-client.yaml">

<violation number="1" location=".github/workflows/samples-kotlin-client.yaml:69">
P2: New Kotlin sample was added to the CI matrix without adding a matching workflow trigger path, so changes in that sample directory can bypass this CI workflow.</violation>
</file>

<file name="samples/client/others/kotlin-jvm-spring-3-restclient-nullable-return/docs/DefaultApi.md">

<violation number="1" location="samples/client/others/kotlin-jvm-spring-3-restclient-nullable-return/docs/DefaultApi.md:53">
P2: `returnNullableString` is documented as non-null in signature/return type, but nullable in the example, creating an API contract mismatch.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread .github/workflows/samples-kotlin-client.yaml
…in api_doc template

- Add workflow trigger path for kotlin-jvm-spring-3-restclient-nullable-return
  sample so changes in that directory actually run CI.
- Propagate nullableReturnType into the signature and return-type sections
  of api_doc.mustache (previously only the example code showed the '?').
- Regenerate DefaultApi.md for the new sample to reflect the fix.
@wing328
Copy link
Copy Markdown
Member

wing328 commented Apr 24, 2026

please follow step 3 to update the samples so as to fix https://github.com/OpenAPITools/openapi-generator/actions/runs/24888307974/job/72874225141?pr=23613

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG][KOTLIN][jvm-spring-restclient] NullPointerException on empty response body despite nullableReturnType=true

2 participants