Skip to content

Commit 9300b95

Browse files
authored
[KOTLIN-SPRING/KOTLIN-CLIENT] BUG - fix json deserialization when kotlin attribute name differs from json attribute name (#24036)
* add @PARAM:JsonProperty * add fix also for kotlin-client and add unit tests * add test open api spec * fix failing test by extending the lookup window
1 parent f854cdf commit 9300b95

255 files changed

Lines changed: 1197 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
@SerializedName("{{{vendorExtensions.x-base-name-literal}}}")
1010
{{/gson}}
1111
{{#jackson}}
12+
@param:JsonProperty("{{{vendorExtensions.x-base-name-literal}}}")
1213
@get:JsonProperty("{{{vendorExtensions.x-base-name-literal}}}")
1314
{{/jackson}}
1415
{{#kotlinx_serialization}}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
@SerializedName("{{{vendorExtensions.x-base-name-literal}}}")
1010
{{/gson}}
1111
{{#jackson}}
12+
@param:JsonProperty("{{{vendorExtensions.x-base-name-literal}}}")
1213
@get:JsonProperty("{{{vendorExtensions.x-base-name-literal}}}")
1314
{{/jackson}}
1415
{{#kotlinx_serialization}}

modules/openapi-generator/src/main/resources/kotlin-spring/dataClassOptVar.mustache

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
@field:JsonInclude(JsonInclude.Include.NON_NULL){{/isNullable}}{{#vendorExtensions.x-has-json-setter-nulls-skip}}
77
@field:JsonSetter(nulls = Nulls.SKIP){{/vendorExtensions.x-has-json-setter-nulls-skip}}{{#vendorExtensions.x-has-json-setter-nulls-fail}}
88
@field:JsonSetter(nulls = Nulls.FAIL){{/vendorExtensions.x-has-json-setter-nulls-fail}}
9+
@param:JsonProperty("{{{baseName}}}")
910
@get:JsonProperty("{{{baseName}}}"){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#vendorExtensions.x-is-jackson-optional-nullable}}JsonNullable<{{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}>{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}?{{/vendorExtensions.x-is-jackson-optional-nullable}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}JsonNullable.undefined(){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^defaultValue}}null{{/defaultValue}}{{#defaultValue}}{{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}{{/vendorExtensions.x-is-jackson-optional-nullable}}

modules/openapi-generator/src/main/resources/kotlin-spring/dataClassReqVar.mustache

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
@Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeInNormalString}}{{{.}}}{{/lambdaEscapeInNormalString}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}
33
@ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeInNormalString}}{{{.}}}{{/lambdaEscapeInNormalString}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{#vendorExtensions.x-field-extra-annotation}}
44
{{{.}}}{{/vendorExtensions.x-field-extra-annotation}}
5+
@param:JsonProperty("{{{baseName}}}")
56
@get:JsonProperty("{{{baseName}}}", required = true){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInPascalCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{^isNumber}}{{{defaultValue}}}{{/isNumber}}{{#isNumber}}{{{dataType}}}("{{{defaultValue}}}"){{/isNumber}}{{/defaultValue}}

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,53 @@ public void testCompanionObjectGeneratesCompanionInModel() throws IOException {
10371037
TestUtils.assertFileContains(petModel, "companion object { }");
10381038
}
10391039

1040+
@Test(description = "nameMappings: @param:JsonProperty must use the original JSON field name for deserialization")
1041+
public void paramJsonPropertyAnnotationWithNameMappings() throws IOException {
1042+
// When a property is renamed via nameMappings, @param:JsonProperty must carry the
1043+
// original JSON field name so Jackson can deserialize from the correct JSON key.
1044+
File output = Files.createTempDirectory("test").toFile();
1045+
output.deleteOnExit();
1046+
1047+
final CodegenConfigurator configurator = new CodegenConfigurator()
1048+
.setGeneratorName("kotlin")
1049+
.setInputSpec("src/test/resources/3_0/kotlin/param-json-property.yaml")
1050+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"))
1051+
.addAdditionalProperty(CodegenConstants.SERIALIZATION_LIBRARY, "jackson")
1052+
.addNameMapping("snake_case_value", "mappedValue");
1053+
1054+
DefaultGenerator generator = new DefaultGenerator();
1055+
generator.opts(configurator.toClientOptInput()).generate();
1056+
1057+
Path itemModel = Paths.get(output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/client/models/Item.kt");
1058+
// @param:JsonProperty must reference the original JSON key, not the mapped Kotlin name
1059+
TestUtils.assertFileContains(itemModel,
1060+
"@param:JsonProperty(\"snake_case_value\")\n @get:JsonProperty(\"snake_case_value\")\n val mappedValue");
1061+
}
1062+
1063+
@Test(description = "auto-renamed digit-starting property: @param:JsonProperty must use the original JSON field name")
1064+
public void paramJsonPropertyAnnotationWithDigitStartingPropertyName() throws IOException {
1065+
// When a property name starts with a digit, the Kotlin codegen wraps it in backticks
1066+
// (e.g. "2nd_field" -> `2ndField`). @param:JsonProperty must still carry the original
1067+
// JSON field name so that Jackson can deserialize it correctly.
1068+
File output = Files.createTempDirectory("test").toFile();
1069+
output.deleteOnExit();
1070+
1071+
final CodegenConfigurator configurator = new CodegenConfigurator()
1072+
.setGeneratorName("kotlin")
1073+
.setInputSpec("src/test/resources/3_0/kotlin/param-json-property.yaml")
1074+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"))
1075+
.addAdditionalProperty(CodegenConstants.SERIALIZATION_LIBRARY, "jackson");
1076+
1077+
DefaultGenerator generator = new DefaultGenerator();
1078+
generator.opts(configurator.toClientOptInput()).generate();
1079+
1080+
Path itemModel = Paths.get(output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/client/models/Item.kt");
1081+
// @param:JsonProperty must reference the original JSON key even when the property
1082+
// is auto-renamed to a backtick-escaped identifier
1083+
TestUtils.assertFileContains(itemModel,
1084+
"@param:JsonProperty(\"2nd_field\")\n @get:JsonProperty(\"2nd_field\")\n val `2ndField`");
1085+
}
1086+
10401087
private static class ModelNameTest {
10411088
private final String expectedName;
10421089
private final String expectedClassName;

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6476,7 +6476,7 @@ public void requiredNullable_scenario3_optionalNonNullable_withDefault() throws
64766476
String content = Files.readString(modelFile);
64776477
int idx = content.indexOf("val optionalNonNullableWithDefault:");
64786478
Assert.assertTrue(idx >= 0, "optionalNonNullableWithDefault property must exist");
6479-
String context = content.substring(Math.max(0, idx - 200), idx);
6479+
String context = content.substring(Math.max(0, idx - 300), idx);
64806480
Assert.assertTrue(context.contains("@field:JsonInclude(JsonInclude.Include.NON_NULL)"),
64816481
"optionalNonNullableWithDefault must have @JsonInclude(NON_NULL)");
64826482
Assert.assertTrue(context.contains("@field:JsonSetter(nulls = Nulls.SKIP)"),
@@ -6777,4 +6777,40 @@ public void schemaMappingWithNullableAllOfRendersNullableKotlinProperty() throws
67776777
String content = Files.readString(myObjectFile.toPath());
67786778
assertThat(content).contains("com.example.ExternalModel?");
67796779
}
6780+
6781+
@Test(description = "nameMappings: @param:JsonProperty must use the original JSON field name for deserialization")
6782+
public void paramJsonPropertyAnnotationWithNameMappings() throws IOException {
6783+
// When a property is renamed via nameMappings, @param:JsonProperty must carry the
6784+
// original JSON field name so Jackson can deserialize from the correct JSON key.
6785+
Map<String, File> files = generateFromContract(
6786+
"src/test/resources/3_0/kotlin/param-json-property.yaml",
6787+
new HashMap<>(),
6788+
new HashMap<>(),
6789+
configurator -> configurator.addNameMapping("snake_case_value", "mappedValue")
6790+
);
6791+
6792+
File itemFile = files.get("Item.kt");
6793+
assertThat(itemFile).isNotNull();
6794+
assertFileContains(
6795+
itemFile.toPath(),
6796+
"@param:JsonProperty(\"snake_case_value\")\n @get:JsonProperty(\"snake_case_value\", required = true) val mappedValue"
6797+
);
6798+
}
6799+
6800+
@Test(description = "auto-renamed digit-starting property: @param:JsonProperty must use the original JSON field name")
6801+
public void paramJsonPropertyAnnotationWithDigitStartingPropertyName() throws IOException {
6802+
// When a property name starts with a digit, the Kotlin codegen wraps it in backticks
6803+
// (e.g. "2nd_field" -> `2ndField`). @param:JsonProperty must still carry the original
6804+
// JSON field name so that Jackson can deserialize it correctly.
6805+
Map<String, File> files = generateFromContract(
6806+
"src/test/resources/3_0/kotlin/param-json-property.yaml"
6807+
);
6808+
6809+
File itemFile = files.get("Item.kt");
6810+
assertThat(itemFile).isNotNull();
6811+
assertFileContains(
6812+
itemFile.toPath(),
6813+
"@param:JsonProperty(\"2nd_field\")\n @get:JsonProperty(\"2nd_field\") val `2ndField`"
6814+
);
6815+
}
67806816
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
openapi: "3.0.0"
2+
info:
3+
title: Test API for @param:JsonProperty annotation
4+
version: "1.0.0"
5+
paths:
6+
/items:
7+
get:
8+
operationId: listItems
9+
responses:
10+
"200":
11+
description: Success
12+
content:
13+
application/json:
14+
schema:
15+
$ref: "#/components/schemas/Item"
16+
components:
17+
schemas:
18+
Item:
19+
type: object
20+
required:
21+
- snake_case_value
22+
properties:
23+
snake_case_value:
24+
type: string
25+
description: A required property to be renamed via nameMappings
26+
2nd_field:
27+
type: string
28+
description: An optional property whose name starts with a digit and is auto-renamed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ import com.fasterxml.jackson.annotation.JsonProperty
3737

3838
data class Bird (
3939

40+
@param:JsonProperty("size")
4041
@get:JsonProperty("size")
4142
val propertySize: kotlin.String? = null,
4243

44+
@param:JsonProperty("color")
4345
@get:JsonProperty("color")
4446
val color: kotlin.String? = null
4547

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ import com.fasterxml.jackson.annotation.JsonProperty
3737

3838
data class Category (
3939

40+
@param:JsonProperty("id")
4041
@get:JsonProperty("id")
4142
val id: kotlin.Long? = null,
4243

44+
@param:JsonProperty("name")
4345
@get:JsonProperty("name")
4446
val name: kotlin.String? = null
4547

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,27 +44,35 @@ import com.fasterxml.jackson.annotation.JsonProperty
4444

4545
data class DefaultValue (
4646

47+
@param:JsonProperty("array_string_enum_ref_default")
4748
@get:JsonProperty("array_string_enum_ref_default")
4849
val arrayStringEnumRefDefault: kotlin.collections.List<StringEnumRef>? = null,
4950

51+
@param:JsonProperty("array_string_enum_default")
5052
@get:JsonProperty("array_string_enum_default")
5153
val arrayStringEnumDefault: kotlin.collections.List<DefaultValue.ArrayStringEnumDefault>? = null,
5254

55+
@param:JsonProperty("array_string_default")
5356
@get:JsonProperty("array_string_default")
5457
val arrayStringDefault: kotlin.collections.List<kotlin.String>? = arrayListOf("failure","skipped"),
5558

59+
@param:JsonProperty("array_integer_default")
5660
@get:JsonProperty("array_integer_default")
5761
val arrayIntegerDefault: kotlin.collections.List<kotlin.Int>? = arrayListOf(1,3),
5862

63+
@param:JsonProperty("array_string")
5964
@get:JsonProperty("array_string")
6065
val arrayString: kotlin.collections.List<kotlin.String>? = null,
6166

67+
@param:JsonProperty("array_string_nullable")
6268
@get:JsonProperty("array_string_nullable")
6369
val arrayStringNullable: kotlin.collections.List<kotlin.String>? = null,
6470

71+
@param:JsonProperty("array_string_extension_nullable")
6572
@get:JsonProperty("array_string_extension_nullable")
6673
val arrayStringExtensionNullable: kotlin.collections.List<kotlin.String>? = null,
6774

75+
@param:JsonProperty("string_nullable")
6876
@get:JsonProperty("string_nullable")
6977
val stringNullable: kotlin.String? = null
7078

0 commit comments

Comments
 (0)