Skip to content

Commit 3504c25

Browse files
committed
[kotlin][jvm-spring-restclient] honour nullableReturnType in body access
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.
1 parent 3971b4f commit 3504c25

31 files changed

Lines changed: 1182 additions & 14 deletions

File tree

.github/workflows/samples-kotlin-client.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ jobs:
6666
- samples/client/echo_api/kotlin-jvm-spring-3-webclient
6767
- samples/client/petstore/kotlin-jvm-spring-3-restclient
6868
- samples/client/echo_api/kotlin-jvm-spring-3-restclient
69+
- samples/client/others/kotlin-jvm-spring-3-restclient-nullable-return
6970
- samples/client/petstore/kotlin-name-parameter-mappings
7071
- samples/client/others/kotlin-jvm-okhttp-parameter-tests
7172
- samples/client/others/kotlin-jvm-okhttp-path-comments
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
generatorName: kotlin
2+
outputDir: samples/client/others/kotlin-jvm-spring-3-restclient-nullable-return
3+
library: jvm-spring-restclient
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/nullable-return-type.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/kotlin-client
6+
additionalProperties:
7+
artifactId: kotlin-jvm-spring-3-restclient-nullable-return
8+
enumUnknownDefaultCase: true
9+
serializationLibrary: jackson
10+
useSpringBoot3: true
11+
nullableReturnType: true

bin/utils/test_file_list.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,5 @@
6666
sha256: 82a6be39c1ed3dada96dfa1833a6709834cb3f9f9d50a19cbd9d49699e46df4f
6767
- filename: "samples/client/petstore/kotlin-jvm-spring-3-restclient/src/test/kotlin/org/openapitools/integration/UserApiTest.kt"
6868
sha256: bc64fb94857a3598e1332f1278307c3078ea9ec4b4aa75690e6eda86e9729a8d
69+
- filename: "samples/client/others/kotlin-jvm-spring-3-restclient-nullable-return/src/test/kotlin/org/openapitools/integration/NullableReturnTypeTest.kt"
70+
sha256: 41ad4769f4d1bbf616056865deca44ed6dc414ca20d43e38fd9d40a1a81e9c19

modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import {{packageName}}.infrastructure.*
6969
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}{{operationIdCamelCase}}>{{/isContainer}}{{^isContainer}}{{enumName}}{{operationIdCamelCase}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{#defaultValue}} = {{>param_default_value}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}): {{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}} {
7070
{{#returnType}}val result = {{/returnType}}{{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}})
7171
{{#returnType}}
72-
return result.body!!
72+
return result.body{{^nullableReturnType}}!!{{/nullableReturnType}}
7373
{{/returnType}}
7474
}
7575

@@ -79,7 +79,7 @@ import {{packageName}}.infrastructure.*
7979
{{/isDeprecated}}
8080
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun {{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}{{operationIdCamelCase}}>{{/isContainer}}{{^isContainer}}{{enumName}}{{operationIdCamelCase}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{#defaultValue}} = {{>param_default_value}}{{/defaultValue}}{{^defaultValue}} = null{{/defaultValue}}{{/required}}{{^-last}}, {{/-last}}{{/allParams}}): ResponseEntity<{{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}}> {
8181
val localVariableConfig = {{operationId}}RequestConfig({{#allParams}}{{{paramName}}} = {{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}})
82-
return request<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map<String, PartConfig<*>>{{/hasFormParams}}{{/hasBodyParam}}, {{{returnType}}}{{^returnType}}Unit{{/returnType}}>(
82+
return request<{{#hasBodyParam}}{{#bodyParams}}{{{dataType}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{^hasFormParams}}Unit{{/hasFormParams}}{{#hasFormParams}}Map<String, PartConfig<*>>{{/hasFormParams}}{{/hasBodyParam}}, {{#returnType}}{{{returnType}}}{{#nullableReturnType}}?{{/nullableReturnType}}{{/returnType}}{{^returnType}}Unit{{/returnType}}>(
8383
localVariableConfig
8484
)
8585
}

modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/infrastructure/ApiClient.kt.mustache

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import org.springframework.util.LinkedMultiValueMap
1010

1111
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}open class ApiClient(protected val client: RestClient) {
1212
13-
protected inline fun <reified I : Any, reified T: Any> request(requestConfig: RequestConfig<I>): ResponseEntity<T> {
13+
protected inline fun <reified I : Any?, reified T: Any?> request(requestConfig: RequestConfig<I>): ResponseEntity<T> {
1414
return prepare(defaults(requestConfig))
1515
.retrieve()
1616
.toEntity(object : ParameterizedTypeReference<T>() {})
1717
}
1818

19-
protected fun <I : Any> prepare(requestConfig: RequestConfig<I>) =
19+
protected fun <I : Any?> prepare(requestConfig: RequestConfig<I>) =
2020
client.method(requestConfig)
2121
.uri(requestConfig)
2222
.headers(requestConfig)
@@ -45,7 +45,7 @@ import org.springframework.util.LinkedMultiValueMap
4545
private fun <I> RestClient.RequestBodySpec.headers(requestConfig: RequestConfig<I>) =
4646
apply { requestConfig.headers.forEach { (name, value) -> header(name, value) } }
4747

48-
private fun <I : Any> RestClient.RequestBodySpec.nullableBody(requestConfig: RequestConfig<I>): RestClient.RequestBodySpec {
48+
private fun <I : Any?> RestClient.RequestBodySpec.nullableBody(requestConfig: RequestConfig<I>): RestClient.RequestBodySpec {
4949
when {
5050
requestConfig.headers[HttpHeaders.CONTENT_TYPE] == MediaType.MULTIPART_FORM_DATA_VALUE -> {
5151
val parts = LinkedMultiValueMap<String, Any>()
@@ -59,7 +59,7 @@ import org.springframework.util.LinkedMultiValueMap
5959
}
6060

6161
else -> {
62-
return apply { if (requestConfig.body != null) body(requestConfig.body) }
62+
return apply { if (requestConfig.body != null) body(requestConfig.body as Any) }
6363
}
6464
}
6565
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Nullable Return Type Test
4+
version: 1.0.0
5+
paths:
6+
/nullable-string:
7+
post:
8+
operationId: returnNullableString
9+
requestBody:
10+
required: true
11+
content:
12+
application/json:
13+
schema:
14+
$ref: '#/components/schemas/PingRequest'
15+
responses:
16+
"200":
17+
description: Successful response, body may be empty
18+
content:
19+
text/html:
20+
schema:
21+
type: string
22+
/always-empty:
23+
get:
24+
operationId: returnNothing
25+
responses:
26+
"204":
27+
description: No content
28+
components:
29+
schemas:
30+
PingRequest:
31+
type: object
32+
properties:
33+
msg:
34+
type: string

samples/client/echo_api/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import org.springframework.util.LinkedMultiValueMap
1010

1111
open class ApiClient(protected val client: RestClient) {
1212

13-
protected inline fun <reified I : Any, reified T: Any> request(requestConfig: RequestConfig<I>): ResponseEntity<T> {
13+
protected inline fun <reified I : Any?, reified T: Any?> request(requestConfig: RequestConfig<I>): ResponseEntity<T> {
1414
return prepare(defaults(requestConfig))
1515
.retrieve()
1616
.toEntity(object : ParameterizedTypeReference<T>() {})
1717
}
1818

19-
protected fun <I : Any> prepare(requestConfig: RequestConfig<I>) =
19+
protected fun <I : Any?> prepare(requestConfig: RequestConfig<I>) =
2020
client.method(requestConfig)
2121
.uri(requestConfig)
2222
.headers(requestConfig)
@@ -45,7 +45,7 @@ open class ApiClient(protected val client: RestClient) {
4545
private fun <I> RestClient.RequestBodySpec.headers(requestConfig: RequestConfig<I>) =
4646
apply { requestConfig.headers.forEach { (name, value) -> header(name, value) } }
4747

48-
private fun <I : Any> RestClient.RequestBodySpec.nullableBody(requestConfig: RequestConfig<I>): RestClient.RequestBodySpec {
48+
private fun <I : Any?> RestClient.RequestBodySpec.nullableBody(requestConfig: RequestConfig<I>): RestClient.RequestBodySpec {
4949
when {
5050
requestConfig.headers[HttpHeaders.CONTENT_TYPE] == MediaType.MULTIPART_FORM_DATA_VALUE -> {
5151
val parts = LinkedMultiValueMap<String, Any>()
@@ -59,7 +59,7 @@ open class ApiClient(protected val client: RestClient) {
5959
}
6060

6161
else -> {
62-
return apply { if (requestConfig.body != null) body(requestConfig.body) }
62+
return apply { if (requestConfig.body != null) body(requestConfig.body as Any) }
6363
}
6464
}
6565
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# OpenAPI Generator Ignore
2+
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
3+
4+
# Use this file to prevent files from being overwritten by the generator.
5+
# The patterns follow closely to .gitignore or .dockerignore.
6+
7+
# As an example, the C# client generator defines ApiClient.cs.
8+
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
9+
#ApiClient.cs
10+
11+
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
12+
#foo/*/qux
13+
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
14+
15+
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
16+
#foo/**/qux
17+
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
18+
19+
# You can also negate patterns with an exclamation (!).
20+
# For example, you can ignore all files in a docs folder with the file extension .md:
21+
#docs/*.md
22+
# Then explicitly reverse the ignore rule for a single file:
23+
#!docs/README.md
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
README.md
2+
build.gradle
3+
docs/DefaultApi.md
4+
docs/PingRequest.md
5+
gradle/wrapper/gradle-wrapper.jar
6+
gradle/wrapper/gradle-wrapper.properties
7+
gradlew
8+
gradlew.bat
9+
settings.gradle
10+
src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt
11+
src/main/kotlin/org/openapitools/client/infrastructure/ApiAbstractions.kt
12+
src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt
13+
src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt
14+
src/main/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt
15+
src/main/kotlin/org/openapitools/client/infrastructure/RequestMethod.kt
16+
src/main/kotlin/org/openapitools/client/infrastructure/Serializer.kt
17+
src/main/kotlin/org/openapitools/client/models/PingRequest.kt
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
7.22.0-SNAPSHOT

0 commit comments

Comments
 (0)