Skip to content

Commit ff663f1

Browse files
authored
[kotlin-client][jvm-ktor] Support nullable response types (OpenAPITools#23870)
The jvm-ktor library did not handle OpenAPI schemas that allow null as a valid response body (e.g. anyOf: [T, {type: null}] or allOf: [T] with nullable: true). Two template bugs prevented this from working: 1. api.mustache used {{{returnType}}} directly, ignoring the isNullable flag on the response property. Now it appends ? when returnProperty.isNullable is true. 2. HttpResponse.kt.mustache hardcoded <T : Any> on every generic type parameter. This made HttpResponse<SomeType?> invalid at the Kotlin type-system level. Removing the : Any constraint lets the generated code use nullable types naturally.
1 parent c1bc76a commit ff663f1

2 files changed

Lines changed: 11 additions & 11 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
5252
{{#returnType}}
5353
@Suppress("UNCHECKED_CAST")
5454
{{/returnType}}
55-
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}open suspend fun {{operationId}}({{#allParams}}{{{paramName}}}: {{{dataType}}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}): HttpResponse<{{{returnType}}}{{^returnType}}Unit{{/returnType}}> {
55+
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}open suspend fun {{operationId}}({{#allParams}}{{{paramName}}}: {{{dataType}}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}): HttpResponse<{{{returnType}}}{{^returnType}}Unit{{/returnType}}{{#returnProperty}}{{#isNullable}}?{{/isNullable}}{{/returnProperty}}> {
5656
5757
val localVariableAuthNames = listOf<String>({{#authMethods}}"{{name}}"{{^-last}}, {{/-last}}{{/authMethods}})
5858

modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/infrastructure/HttpResponse.kt.mustache

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import io.ktor.http.isSuccess
55
import io.ktor.util.reflect.TypeInfo
66
import io.ktor.util.reflect.typeInfo
77

8-
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}open class HttpResponse<T : Any>({{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}val response: io.ktor.client.statement.HttpResponse, {{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}val provider: BodyProvider<T>) {
8+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}open class HttpResponse<T>({{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}val response: io.ktor.client.statement.HttpResponse, {{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}val provider: BodyProvider<T>) {
99
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}val status: Int = response.status.value
1010
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}val success: Boolean = response.status.isSuccess()
1111
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}val headers: Map<String, List<String>> = response.headers.mapEntries()
1212
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}suspend fun body(): T = provider.body(response)
13-
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}suspend fun <V : Any> typedBody(type: TypeInfo): V = provider.typedBody(response, type)
13+
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}suspend fun <V> typedBody(type: TypeInfo): V = provider.typedBody(response, type)
1414

1515
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}companion object {
1616
private fun Headers.mapEntries(): Map<String, List<String>> {
@@ -21,31 +21,31 @@ import io.ktor.util.reflect.typeInfo
2121
}
2222
}
2323

24-
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}interface BodyProvider<T : Any> {
24+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}interface BodyProvider<T> {
2525
suspend fun body(response: io.ktor.client.statement.HttpResponse): T
26-
suspend fun <V : Any> typedBody(response: io.ktor.client.statement.HttpResponse, type: TypeInfo): V
26+
suspend fun <V> typedBody(response: io.ktor.client.statement.HttpResponse, type: TypeInfo): V
2727
}
2828

29-
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}class TypedBodyProvider<T : Any>(private val type: TypeInfo) : BodyProvider<T> {
29+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}class TypedBodyProvider<T>(private val type: TypeInfo) : BodyProvider<T> {
3030
@Suppress("UNCHECKED_CAST")
3131
override suspend fun body(response: io.ktor.client.statement.HttpResponse): T =
3232
response.call.body(type) as T
3333
3434
@Suppress("UNCHECKED_CAST")
35-
override suspend fun <V : Any> typedBody(response: io.ktor.client.statement.HttpResponse, type: TypeInfo): V =
35+
override suspend fun <V> typedBody(response: io.ktor.client.statement.HttpResponse, type: TypeInfo): V =
3636
response.call.body(type) as V
3737
}
3838

39-
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}class MappedBodyProvider<S : Any, T : Any>(private val provider: BodyProvider<S>, private val block: S.() -> T) : BodyProvider<T> {
39+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}class MappedBodyProvider<S, T>(private val provider: BodyProvider<S>, private val block: S.() -> T) : BodyProvider<T> {
4040
override suspend fun body(response: io.ktor.client.statement.HttpResponse): T =
4141
block(provider.body(response))
4242
43-
override suspend fun <V : Any> typedBody(response: io.ktor.client.statement.HttpResponse, type: TypeInfo): V =
43+
override suspend fun <V> typedBody(response: io.ktor.client.statement.HttpResponse, type: TypeInfo): V =
4444
provider.typedBody(response, type)
4545
}
4646

47-
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}inline fun <reified T : Any> io.ktor.client.statement.HttpResponse.wrap(): HttpResponse<T> =
47+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}inline fun <reified T> io.ktor.client.statement.HttpResponse.wrap(): HttpResponse<T> =
4848
HttpResponse(this, TypedBodyProvider(typeInfo<T>()))
4949

50-
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun <T : Any, V : Any> HttpResponse<T>.map(block: T.() -> V): HttpResponse<V> =
50+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun <T, V> HttpResponse<T>.map(block: T.() -> V): HttpResponse<V> =
5151
HttpResponse(response, MappedBodyProvider(provider, block))

0 commit comments

Comments
 (0)