Skip to content

Commit 621ccff

Browse files
authored
[kotlin] Fix the kotlinx serializer for a number branch in oneOf/anyOf wrappers (#25472)
* [kotlin-client] Encode unformatted number branches in kotlinx oneOf/anyOf wrappers A plain `type: number` member maps to java.math.BigDecimal, which is not a Kotlin primitive, so the kotlinx_serialization oneOf/anyOf wrapper serializer emitted `java.math.BigDecimal.serializer()`. kotlinx.serialization has no such serializer, so the generated code did not compile. The number branch now writes the value as a JSON number via `JsonPrimitive`. Fixes #25452 * [kotlin-client] Decode number branches in kotlinx oneOf/anyOf wrappers The unformatted number (java.math.BigDecimal) branch is now encoded as a JSON number, but its decode branch only ran for non-primitive JSON elements, so a number never matched. A plain Json also has no BigDecimal serializer for decodeFromJsonElement. The branch now accepts a non-string JsonPrimitive and builds the BigDecimal from its content.
1 parent cc39f62 commit 621ccff

4 files changed

Lines changed: 96 additions & 0 deletions

File tree

‎modules/openapi-generator/src/main/resources/kotlin-client/anyof_class.mustache‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,12 @@ import java.io.IOException
156156
{{/isNumber}}
157157
{{/isPrimitiveType}}
158158
{{^isPrimitiveType}}
159+
{{#isNumber}}
160+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeJsonElement(JsonPrimitive(value.value))
161+
{{/isNumber}}
162+
{{^isNumber}}
159163
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeSerializableValue({{{dataType}}}.serializer(), value.value)
164+
{{/isNumber}}
160165
{{/isPrimitiveType}}
161166
{{/isArray}}
162167
{{/vendorExtensions.x-duplicated-data-type}}
@@ -208,6 +213,16 @@ import java.io.IOException
208213
{{/isString}}
209214
{{/isPrimitiveType}}
210215
{{^isPrimitiveType}}
216+
{{#isNumber}}
217+
if (jsonElement is JsonPrimitive && !jsonElement.isString) {
218+
try {
219+
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(jsonElement.content.toBigDecimal())
220+
} catch (e: Exception) {
221+
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
222+
}
223+
}
224+
{{/isNumber}}
225+
{{^isNumber}}
211226
if (jsonElement !is JsonPrimitive) {
212227
try {
213228
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
@@ -216,6 +231,7 @@ import java.io.IOException
216231
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
217232
}
218233
}
234+
{{/isNumber}}
219235
{{/isPrimitiveType}}
220236
{{/isArray}}
221237
{{/vendorExtensions.x-duplicated-data-type}}

‎modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,12 @@ import java.io.IOException
219219
{{/isNumber}}
220220
{{/isPrimitiveType}}
221221
{{^isPrimitiveType}}
222+
{{#isNumber}}
223+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeJsonElement(JsonPrimitive(value.value))
224+
{{/isNumber}}
225+
{{^isNumber}}
222226
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeSerializableValue({{{dataType}}}.serializer(), value.value)
227+
{{/isNumber}}
223228
{{/isPrimitiveType}}
224229
{{/isArray}}
225230
{{/vendorExtensions.x-duplicated-data-type}}
@@ -271,6 +276,16 @@ import java.io.IOException
271276
{{/isString}}
272277
{{/isPrimitiveType}}
273278
{{^isPrimitiveType}}
279+
{{#isNumber}}
280+
if (jsonElement is JsonPrimitive && !jsonElement.isString) {
281+
try {
282+
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(jsonElement.content.toBigDecimal())
283+
} catch (e: Exception) {
284+
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
285+
}
286+
}
287+
{{/isNumber}}
288+
{{^isNumber}}
274289
if (jsonElement !is JsonPrimitive) {
275290
try {
276291
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
@@ -279,6 +294,7 @@ import java.io.IOException
279294
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
280295
}
281296
}
297+
{{/isNumber}}
282298
{{/isPrimitiveType}}
283299
{{/isArray}}
284300
{{/vendorExtensions.x-duplicated-data-type}}

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,4 +1582,33 @@ public void testEnumUnknownDefaultCaseIsRegisteredAsCliOption() {
15821582
Assert.assertEquals(option.getDefault(), "false");
15831583
Assert.assertEquals(option.getEnum().keySet(), Set.of("true", "false"));
15841584
}
1585+
1586+
@Test(description = "Issue #25452: unformatted number in kotlinx_serialization oneOf/anyOf wrappers is encoded and decoded without a BigDecimal serializer")
1587+
public void oneOfAnyOfKotlinxSerializationUnformattedNumber() throws IOException {
1588+
File output = Files.createTempDirectory("test").toFile();
1589+
output.deleteOnExit();
1590+
1591+
new DefaultGenerator()
1592+
.opts(new CodegenConfigurator()
1593+
.setGeneratorName(KOTLIN_GENERATOR)
1594+
.setLibrary("jvm-retrofit2")
1595+
.addAdditionalProperty(CodegenConstants.SERIALIZATION_LIBRARY, "kotlinx_serialization")
1596+
.addAdditionalProperty(GENERATE_ONEOF_ANYOF_WRAPPERS, true)
1597+
.setInputSpec("src/test/resources/3_0/kotlin/issue25452-anyof-oneof-number.yaml")
1598+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"))
1599+
.toClientOptInput())
1600+
.generate();
1601+
1602+
for (String model : new String[]{"AnyOfValue", "OneOfValue"}) {
1603+
// kotlinx.serialization has no serializer for java.math.BigDecimal, so this call cannot compile
1604+
TestUtils.assertFileNotContains(modelPath(output, model), "java.math.BigDecimal.serializer()");
1605+
TestUtils.assertFileContains(modelPath(output, model),
1606+
"is " + model + ".BigDecimalValue -> jsonEncoder.encodeJsonElement(JsonPrimitive(value.value))");
1607+
// the number is a JsonPrimitive and a plain Json has no BigDecimal serializer, so it is read from the content
1608+
TestUtils.assertFileNotContains(modelPath(output, model), "decodeFromJsonElement<java.math.BigDecimal>");
1609+
TestUtils.assertFileContains(modelPath(output, model),
1610+
"if (jsonElement is JsonPrimitive && !jsonElement.isString) { try { return " + model
1611+
+ ".BigDecimalValue(jsonElement.content.toBigDecimal())");
1612+
}
1613+
}
15851614
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
openapi: 3.0.3
2+
info:
3+
title: String or number
4+
version: 1.0.0
5+
paths:
6+
/any:
7+
get:
8+
operationId: getAnyOf
9+
responses:
10+
"200":
11+
description: A string or a number
12+
content:
13+
application/json:
14+
schema:
15+
$ref: "#/components/schemas/AnyOfValue"
16+
/one:
17+
get:
18+
operationId: getOneOf
19+
responses:
20+
"200":
21+
description: A string or a number
22+
content:
23+
application/json:
24+
schema:
25+
$ref: "#/components/schemas/OneOfValue"
26+
components:
27+
schemas:
28+
AnyOfValue:
29+
anyOf:
30+
- type: string
31+
- type: number
32+
OneOfValue:
33+
oneOf:
34+
- type: string
35+
- type: number

0 commit comments

Comments
 (0)