From 0aedd88c5b93858031f474062386de9b146bbd03 Mon Sep 17 00:00:00 2001 From: luisfdemarchi Date: Wed, 26 Nov 2025 15:08:58 +0100 Subject: [PATCH] [kotlin-spring][server]fix generation of referenced examples on RequestBody # Conflicts: # modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java --- .../languages/KotlinSpringServerCodegen.java | 15 +++++ .../main/resources/kotlin-spring/api.mustache | 1 + .../kotlin-spring/apiInterface.mustache | 1 + .../kotlin-spring/requestBody.mustache | 15 +++++ .../assertions/MethodAnnotationAssert.java | 18 ++++++ .../kotlin/assertions/MethodAssert.java | 14 +++++ .../spring/KotlinSpringServerCodegenTest.java | 33 +++++++++++ .../src/test/resources/bugs/issue_20009.yaml | 55 +++++++++++++++++++ 8 files changed, 152 insertions(+) create mode 100644 modules/openapi-generator/src/main/resources/kotlin-spring/requestBody.mustache create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/assertions/MethodAnnotationAssert.java create mode 100644 modules/openapi-generator/src/test/resources/bugs/issue_20009.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java index 62404681c5cb..a7b1f01287c2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java @@ -34,6 +34,7 @@ import org.openapitools.codegen.model.OperationMap; import org.openapitools.codegen.model.OperationsMap; import org.openapitools.codegen.templating.mustache.SpringHttpStatusLambda; +import org.openapitools.codegen.utils.ExamplesUtils; import org.openapitools.codegen.utils.ModelUtils; import org.openapitools.codegen.utils.URLPathUtils; import org.slf4j.Logger; @@ -999,6 +1000,20 @@ public void setReturnContainer(final String returnContainer) { }); }); } + if (operation.bodyParam != null && operation.bodyParam.getContent() != null && !operation.bodyParam.getContent().isEmpty()) { + List> contentList = new ArrayList<>(); + operation.bodyParam.getContent().forEach((mediaType, mediaTypeObject) -> { + Map entry = new HashMap<>(); + entry.put("mediaType", mediaType); + entry.put("schema", mediaTypeObject.getSchema()); + if (mediaTypeObject.getExamples() != null && !mediaTypeObject.getExamples().isEmpty()) { + entry.put("examples", ExamplesUtils.unaliasExamples(openAPI, mediaTypeObject.getExamples())); + } + contentList.add(entry); + }); + operation.bodyParam.vendorExtensions.put("content", contentList); + + } final List allParams = operation.allParams; if (allParams != null) { diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache index e73afb7f3630..7b950687e3df 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/api.mustache @@ -76,6 +76,7 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v summary = "{{{summary}}}", operationId = "{{{operationId}}}", description = """{{{unescapedNotes}}}""", + requestBody = {{>requestBody}}, responses = [{{#responses}} ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = [Content({{#isArray}}array = ArraySchema({{/isArray}}schema = Schema(implementation = {{{baseType}}}::class)){{#isArray}}){{/isArray}}]{{/baseType}}){{^-last}},{{/-last}}{{/responses}} ]{{#hasAuthMethods}}, security = [ {{#authMethods}}SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes = [ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} ]{{/isOAuth}}){{^-last}},{{/-last}}{{/authMethods}} ]{{/hasAuthMethods}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache index 0f1dcbd41551..8cd8348bff61 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/apiInterface.mustache @@ -88,6 +88,7 @@ interface {{classname}} { summary = "{{{summary}}}", operationId = "{{{operationId}}}", description = """{{{unescapedNotes}}}""", + requestBody = {{>requestBody}}, responses = [{{#responses}} ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = [Content({{#isArray}}array = ArraySchema({{/isArray}}schema = Schema(implementation = {{{baseType}}}::class)){{#isArray}}){{/isArray}}]{{/baseType}}){{^-last}},{{/-last}}{{/responses}} ]{{#hasAuthMethods}}, diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/requestBody.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/requestBody.mustache new file mode 100644 index 000000000000..112500c72694 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/requestBody.mustache @@ -0,0 +1,15 @@ +@SwaggerRequestBody( + description = "{{{bodyParam.description}}}", + content = [ + {{#bodyParam.vendorExtensions.content}} + Content( + mediaType = "{{mediaType}}", + examples = [ + {{#examples}} + Example(name = "{{{exampleName}}}", value = "{{{exampleValue}}}"){{^-last}},{{/-last}} + {{/examples}} + ] + ){{^-last}},{{/-last}} + {{/bodyParam.vendorExtensions.content}} + ] +) \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/assertions/MethodAnnotationAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/assertions/MethodAnnotationAssert.java new file mode 100644 index 000000000000..59dab7ae531f --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/assertions/MethodAnnotationAssert.java @@ -0,0 +1,18 @@ +package org.openapitools.codegen.kotlin.assertions; + +import org.assertj.core.util.CanIgnoreReturnValue; +import org.jetbrains.kotlin.psi.KtAnnotationEntry; + +@CanIgnoreReturnValue +public class MethodAnnotationAssert extends AbstractAnnotationAssert { + private final MethodAssert methodAssert; + + MethodAnnotationAssert(final MethodAssert methodAssert, final KtAnnotationEntry annotationEntry) { + super(annotationEntry, MethodAnnotationAssert.class); + this.methodAssert = methodAssert; + } + + public MethodAssert toMethod() { + return methodAssert; + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/assertions/MethodAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/assertions/MethodAssert.java index ed13a950bfed..7f8df5c35fbb 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/assertions/MethodAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/assertions/MethodAssert.java @@ -3,6 +3,7 @@ import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.Assertions; import org.assertj.core.util.CanIgnoreReturnValue; +import org.jetbrains.kotlin.psi.KtAnnotationEntry; import org.jetbrains.kotlin.psi.KtNamedFunction; import org.jetbrains.kotlin.psi.KtParameter; @@ -30,6 +31,19 @@ public ParameterAssert assertParameter(final String parameterName) { return new ParameterAssert(this, parameters.get(0)); } + public MethodAnnotationAssert assertAnnotation(final String annotationName) { + final List annotations = actual.getAnnotationEntries().stream() + .filter( + p -> Objects.equals(p.getTypeReference() != null ? p.getTypeReference().getText() : null, annotationName) + ) + .collect(Collectors.toList()); + Assertions.assertThat(annotations) + .withFailMessage("Expected class to have a single annotation %s, but found %s", annotationName, annotations.size()) + .hasSize(1); + + return new MethodAnnotationAssert(this, annotations.get(0)); + } + public ClassAssert toClass() { return classAssert; } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java index c9843b2a0169..152a5b599e83 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java @@ -3561,4 +3561,37 @@ private Map generateFromContract( return generator.opts(input).generate().stream() .collect(Collectors.toMap(File::getName, Function.identity())); } + + @Test + public void testRequestBodyExamplesAreGenerated() throws IOException { + Map additionalProperties = new HashMap<>(); + additionalProperties.put(ANNOTATION_LIBRARY, AnnotationLibrary.SWAGGER2.toCliOptValue()); + final Map files = generateFromContract("src/test/resources/bugs/issue_20009.yaml", additionalProperties); + KotlinFileAssert.assertThat(files.get("PetsApiController.kt")) + .assertClass("PetsApiController") + .assertMethod("postPet") + .assertAnnotation("Operation") + .hasAttributes(ImmutableMap.of("requestBody", "@SwaggerRequestBody(\n" + + " description = \"Request body to create a pet\",\n" + + " content = [\n" + + " Content(\n" + + " mediaType = \"application/vnd.api.v2+json\",\n" + + " examples = [\n" + + " Example(name = \"V2ReferencedExample\", value = \"{\\\"v2_example_schema_property\\\":\\\"example schema property value from referenced example\\\",\\\"v2_another_example_schema_property\\\":\\\"another example schema property value from referenced example\\\"}\")\n" + + " ]\n" + + " ),\n" + + " Content(\n" + + " mediaType = \"application/vnd.api+json\",\n" + + " examples = [\n" + + " Example(name = \"\", value = \"\\\"example6 value\\\"\"),\n" + + " Example(name = \"ReferencedExample\", value = \"{\\\"example_schema_property\\\":\\\"example schema property value from referenced example\\\",\\\"another_example_schema_property\\\":\\\"another example schema property value from referenced example\\\"}\")\n" + + " ]\n" + + " )\n" + + " ]\n" + + ")") + + ) + ; + } + } diff --git a/modules/openapi-generator/src/test/resources/bugs/issue_20009.yaml b/modules/openapi-generator/src/test/resources/bugs/issue_20009.yaml new file mode 100644 index 000000000000..9a0ac7921640 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/bugs/issue_20009.yaml @@ -0,0 +1,55 @@ +openapi: "3.0.0" +info: + version: 2.0.0 + title: test +paths: + /pets: + post: + operationId: postPet + description: Creates a pet + requestBody: + description: Request body to create a pet + required: true + content: + application/vnd.api.v2+json: + schema: + $ref: '#/components/schemas/ObjectSchema' + examples: + V2ReferencedExample: + $ref: '#/components/examples/V2ReferencedExample' + application/vnd.api+json: + schema: + type: object + properties: + name: + type: string + examples: + NonReferencedExample: + description: "Non referenced Example" + value: 'example6 value' + ReferencedExample: + $ref: '#/components/examples/ReferencedExample' + responses: + '200': + description: successful operation + +components: + schemas: + ObjectSchema: + type: object + properties: + id: + type: integer + name: + type: string + examples: + ReferencedExample: + summary: An example of ReferencedExample + value: + example_schema_property: example schema property value from referenced example + another_example_schema_property: another example schema property value from referenced example + V2ReferencedExample: + summary: An example of ReferencedExample for version 2 + value: + v2_example_schema_property: example schema property value from referenced example + v2_another_example_schema_property: another example schema property value from referenced example \ No newline at end of file