From adbd1be4b3ec11139e0c9efc89b1a61ae0e310e3 Mon Sep 17 00:00:00 2001 From: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:14:28 +0900 Subject: [PATCH] fix(kotlin-server): use valid Kotlin String conversions for ktor delegate header params The ktor delegate-pattern header extraction template emitted it.to() (e.g. it.tokotlin.Boolean(), it.tojava.math.BigDecimal()), which is not valid Kotlin and fails to compile. Kotlin's String conversion functions use the simple type name (toBoolean/toInt/toLong/toDouble/toBigDecimal), so map the primitive type flags to those functions instead of interpolating the dataType. Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> --- .../ktor/_extract_param_value.mustache | 2 +- .../kotlin/KotlinServerCodegenTest.java | 42 +++++++++++++++++++ ...sue24214-ktor-header-param-conversion.yaml | 40 ++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/kotlin/issue24214-ktor-header-param-conversion.yaml diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache index 540d0e4168cb..ea1e7431123c 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache @@ -1 +1 @@ -{{^required}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}}{{/required}}{{#required}}{{#defaultValue}}{{#isString}}{{/isString}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}} ?: {{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}.let { it{{#isString}}{{/isString}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}} ?: throw BadParameterException(message = "Missing {{#isQueryParam}}query{{/isQueryParam}}{{#isPathParam}}path{{/isPathParam}}{{#isHeaderParam}}header{{/isHeaderParam}}{{#isCookieParam}}cookie{{/isCookieParam}}{{#isFormParam}}form{{/isFormParam}} parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") }{{/defaultValue}}{{/required}} \ No newline at end of file +{{^required}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { it.toBigDecimal() }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { {{#isBoolean}}it.toBoolean(){{/isBoolean}}{{#isInteger}}it.toInt(){{/isInteger}}{{#isLong}}it.toLong(){{/isLong}}{{#isFloat}}it.toFloat(){{/isFloat}}{{#isDouble}}it.toDouble(){{/isDouble}}{{^isBoolean}}{{^isInteger}}{{^isLong}}{{^isFloat}}{{^isDouble}}it.to{{{dataType}}}(){{/isDouble}}{{/isFloat}}{{/isLong}}{{/isInteger}}{{/isBoolean}} }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}}{{/required}}{{#required}}{{#defaultValue}}{{#isString}}{{/isString}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { it.toBigDecimal() }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { {{#isBoolean}}it.toBoolean(){{/isBoolean}}{{#isInteger}}it.toInt(){{/isInteger}}{{#isLong}}it.toLong(){{/isLong}}{{#isFloat}}it.toFloat(){{/isFloat}}{{#isDouble}}it.toDouble(){{/isDouble}}{{^isBoolean}}{{^isInteger}}{{^isLong}}{{^isFloat}}{{^isDouble}}it.to{{{dataType}}}(){{/isDouble}}{{/isFloat}}{{/isLong}}{{/isInteger}}{{/isBoolean}} }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}} ?: {{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}.let { it{{#isString}}{{/isString}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { it.toBigDecimal() }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { {{#isBoolean}}it.toBoolean(){{/isBoolean}}{{#isInteger}}it.toInt(){{/isInteger}}{{#isLong}}it.toLong(){{/isLong}}{{#isFloat}}it.toFloat(){{/isFloat}}{{#isDouble}}it.toDouble(){{/isDouble}}{{^isBoolean}}{{^isInteger}}{{^isLong}}{{^isFloat}}{{^isDouble}}it.to{{{dataType}}}(){{/isDouble}}{{/isFloat}}{{/isLong}}{{/isInteger}}{{/isBoolean}} }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}} ?: throw BadParameterException(message = "Missing {{#isQueryParam}}query{{/isQueryParam}}{{#isPathParam}}path{{/isPathParam}}{{#isHeaderParam}}header{{/isHeaderParam}}{{#isCookieParam}}cookie{{/isCookieParam}}{{#isFormParam}}form{{/isFormParam}} parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") }{{/defaultValue}}{{/required}} \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java index 87176dc4eaa3..56ace6613ba4 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java @@ -709,6 +709,48 @@ public void delegatePattern_enumWireValue() throws IOException { ); } + @Test + public void delegatePattern_headerParamPrimitiveConversion() throws IOException { + // Regression test for https://github.com/OpenAPITools/openapi-generator/issues/24214 + // Header params were converted with `it.to()` + // (e.g. `it.tokotlin.Boolean()`, `it.tojava.math.BigDecimal()`), which is not valid Kotlin. + // The correct String extension is `it.to()`. + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(LIBRARY, KTOR); + codegen.additionalProperties().put(DELEGATE_PATTERN, true); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_0/kotlin/issue24214-ktor-header-param-conversion.yaml")) + .config(codegen)) + .generate(); + + Path apiPath = Paths.get(output.getAbsolutePath() + + "/src/main/kotlin/org/openapitools/server/apis/DefaultApi.kt"); + + assertFileContains( + apiPath, + "val boolHeader = call.request.headers[\"bool-header\"]?.let { runCatching { it.toBoolean() }", + "val intHeader = call.request.headers[\"int-header\"]?.let { runCatching { it.toInt() }", + "val longHeader = call.request.headers[\"long-header\"]?.let { runCatching { it.toLong() }", + "val doubleHeader = call.request.headers[\"double-header\"]?.let { runCatching { it.toDouble() }", + "val numberHeader = call.request.headers[\"number-header\"]?.let { runCatching { it.toBigDecimal() }" + ); + + // The old, uncompilable fully-qualified conversions must be gone. + assertFileNotContains( + apiPath, + "it.tokotlin.Boolean()", + "it.tokotlin.Int()", + "it.tokotlin.Long()", + "it.tokotlin.Double()", + "it.tojava.math.BigDecimal()" + ); + } + @Test public void testFloatingPointMultipleOfValidationUsesTolerance() throws IOException { diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/issue24214-ktor-header-param-conversion.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/issue24214-ktor-header-param-conversion.yaml new file mode 100644 index 000000000000..d15f151b28af --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/issue24214-ktor-header-param-conversion.yaml @@ -0,0 +1,40 @@ +openapi: 3.0.3 +info: + title: ktor header param primitive conversion + version: 1.0.0 +paths: + /headers: + get: + operationId: getWithHeaders + parameters: + - name: bool-header + in: header + required: false + schema: + type: boolean + - name: int-header + in: header + required: false + schema: + type: integer + format: int32 + - name: long-header + in: header + required: false + schema: + type: integer + format: int64 + - name: double-header + in: header + required: false + schema: + type: number + format: double + - name: number-header + in: header + required: false + schema: + type: number + responses: + '200': + description: ok