From 8dfaa1f3eb19c2e3f36c0487ee3cca6be120e2bc Mon Sep 17 00:00:00 2001 From: Gianni Giglio Date: Tue, 8 Sep 2026 17:28:55 +0200 Subject: [PATCH] [spring] fix schema named "Schema" colliding with the swagger2 annotation A schema named "Schema" produces a model that cannot be referenced by the generated code, because AbstractJavaCodegen registers importMapping.put("Schema", "io.swagger.v3.oas.annotations.media.Schema") and DefaultGenerator consults importMapping before toModelImport(). The model import is therefore silently replaced by the annotation import, so the bare "Schema" type in api signatures resolves to the annotation. Emitting the model import instead does not help on its own: it then clashes with the annotation import the templates add, and in the model file with the declared class of the same name, which is a compile error either way (JLS 7.5.1). Drop the default mapping when a model of that name exists so the model keeps the simple name, and fully qualify the annotation at its usage sites rather than importing it. An explicit --import-mappings entry pointing at a different class is left untouched. Verified against the spec from the issue: generated project fails to compile on master with 15 errors and compiles cleanly with this change, for both interfaceOnly=true and the api controller variant. Regenerating all 71 spring and java-camel sample configs produces no diff, since the qualification only applies when the collision is present. Fixes #16584 --- .../codegen/languages/SpringCodegen.java | 43 ++++++++++++++ .../main/resources/JavaSpring/api.mustache | 4 +- .../JavaSpring/apiController.mustache | 2 + .../JavaSpring/lombokAnnotation.mustache | 2 +- .../main/resources/JavaSpring/model.mustache | 2 + .../main/resources/JavaSpring/pojo.mustache | 4 +- .../java/spring/SpringCodegenTest.java | 59 ++++++++++++++++++- .../src/test/resources/bugs/issue_16584.yaml | 35 +++++++++++ 8 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/bugs/issue_16584.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 4d8b7f462fcc..b409a4d311f6 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -115,6 +115,15 @@ public class SpringCodegen extends AbstractJavaCodegen public static final String USE_SEALED = "useSealed"; public static final String OPTIONAL_ACCEPT_NULLABLE = "optionalAcceptNullable"; public static final String USE_SPRING_BUILT_IN_VALIDATION = "useSpringBuiltInValidation"; + + /** Default importMapping value registered by {@link AbstractJavaCodegen} for the swagger2 annotation. */ + private static final String SWAGGER2_ANNOTATION_SCHEMA_IMPORT = "io.swagger.v3.oas.annotations.media.Schema"; + + /** Simple or fully-qualified name to use at swagger2 {@code @Schema} annotation usage sites. */ + private static final String SCHEMA_ANNOTATION = "swagger2SchemaAnnotation"; + + /** Whether templates should emit the single-type import for the swagger2 {@code @Schema} annotation. */ + private static final String IMPORT_SCHEMA_ANNOTATION = "importSwagger2SchemaAnnotation"; public static final String SPRING_API_VERSION = "springApiVersion"; public static final String USE_JACKSON_3 = "useJackson3"; public static final String JACKSON2_PACKAGE = "com.fasterxml.jackson"; @@ -937,10 +946,44 @@ public void addOperationToGroup(String tag, String resourcePath, Operation opera } + /** + * Whether any schema in the document produces the given generated model name. + * Compared against {@link #toModelName(String)} so modelNamePrefix/modelNameSuffix are honoured. + */ + private boolean hasModelNamed(OpenAPI openAPI, String modelName) { + if (openAPI == null || openAPI.getComponents() == null || openAPI.getComponents().getSchemas() == null) { + return false; + } + return openAPI.getComponents().getSchemas().keySet().stream() + .anyMatch(schemaName -> modelName.equals(toModelName(schemaName))); + } + @Override public void preprocessOpenAPI(OpenAPI openAPI) { super.preprocessOpenAPI(openAPI); + // A schema named "Schema" collides with io.swagger.v3.oas.annotations.media.Schema. Two + // problems follow: + // 1. AbstractJavaCodegen registers "Schema" -> the annotation FQN in importMapping, and + // importMapping is consulted before toModelImport(), so the model import is silently + // replaced by the annotation import. The bare "Schema" type in api signatures then + // resolves to the annotation instead of the model. + // 2. Emitting the model import alongside the annotation import the templates add is a + // compile error: two single-type imports for the same simple name (JLS 7.5.1). In the + // model file the declared class shadows the import for the same reason. + // Let the model keep the simple name and fully qualify the annotation at its usage sites + // instead of importing it. An explicit --import-mappings entry for "Schema" carries a + // different value and is deliberately left untouched. + additionalProperties.put(SCHEMA_ANNOTATION, "Schema"); + additionalProperties.put(IMPORT_SCHEMA_ANNOTATION, true); + if (hasModelNamed(openAPI, "Schema")) { + if (SWAGGER2_ANNOTATION_SCHEMA_IMPORT.equals(importMapping.get("Schema"))) { + importMapping.remove("Schema"); + } + additionalProperties.put(SCHEMA_ANNOTATION, SWAGGER2_ANNOTATION_SCHEMA_IMPORT); + additionalProperties.put(IMPORT_SCHEMA_ANNOTATION, false); + } + if (SPRING_BOOT.equals(library) && ModelUtils.containsEnums(this.openAPI)) { supportingFiles.add(new SupportingFile("converter.mustache", (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "EnumConverterConfiguration.java")); diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache index 39394a4f1991..8052a8c04eb6 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache @@ -14,7 +14,9 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; +{{#importSwagger2SchemaAnnotation}} import io.swagger.v3.oas.annotations.media.Schema; +{{/importSwagger2SchemaAnnotation}} import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; @@ -187,7 +189,7 @@ public interface {{classname}} { {{#responses}} @ApiResponse(responseCode = {{#isDefault}}"default"{{/isDefault}}{{^isDefault}}"{{{code}}}"{{/isDefault}}, description = "{{{message}}}"{{#baseType}}, content = { {{#produces}} - @Content(mediaType = "{{{mediaType}}}", {{#isArray}}array = @ArraySchema({{/isArray}}schema = @Schema(implementation = {{{baseType}}}.class){{#isArray}}){{/isArray}}{{^isJson}}){{^-last}},{{/-last}}{{/isJson}}{{#isJson}}{{^examples.0}}){{^-last}},{{/-last}}{{/examples.0}}{{#examples.0}}, examples = { + @Content(mediaType = "{{{mediaType}}}", {{#isArray}}array = @ArraySchema({{/isArray}}schema = @{{swagger2SchemaAnnotation}}(implementation = {{{baseType}}}.class){{#isArray}}){{/isArray}}{{^isJson}}){{^-last}},{{/-last}}{{/isJson}}{{#isJson}}{{^examples.0}}){{^-last}},{{/-last}}{{/examples.0}}{{#examples.0}}, examples = { {{#examples}} @ExampleObject( name = "{{{exampleName}}}", diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/apiController.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/apiController.mustache index 7de0ac763bcf..21a5859aa7cf 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/apiController.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/apiController.mustache @@ -8,7 +8,9 @@ package {{package}}; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; +{{#importSwagger2SchemaAnnotation}} import io.swagger.v3.oas.annotations.media.Schema; +{{/importSwagger2SchemaAnnotation}} import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/lombokAnnotation.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/lombokAnnotation.mustache index 6b09b602fe2a..679513d1c0c7 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/lombokAnnotation.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/lombokAnnotation.mustache @@ -21,7 +21,7 @@ {{/required}} {{/useBeanValidation}} {{#swagger2AnnotationLibrary}} - @Schema(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = Schema.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}{{#deprecated}}, deprecated = true{{/deprecated}}, requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}) + @{{swagger2SchemaAnnotation}}(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = {{swagger2SchemaAnnotation}}.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}{{#deprecated}}, deprecated = true{{/deprecated}}, requiredMode = {{#required}}{{swagger2SchemaAnnotation}}.RequiredMode.REQUIRED{{/required}}{{^required}}{{swagger2SchemaAnnotation}}.RequiredMode.NOT_REQUIRED{{/required}}) {{/swagger2AnnotationLibrary}} {{#jackson}}{{>jackson_annotations}}{{/jackson}} {{/lombok.Data}} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/model.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/model.mustache index 7655d0701c0b..d62dc3d9434a 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/model.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/model.mustache @@ -29,7 +29,9 @@ import {{jacksonPackage}}.dataformat.xml.annotation.JacksonXmlElementWrapper; {{/withXml}} {{/jackson}} {{#swagger2AnnotationLibrary}} +{{#importSwagger2SchemaAnnotation}} import io.swagger.v3.oas.annotations.media.Schema; +{{/importSwagger2SchemaAnnotation}} {{/swagger2AnnotationLibrary}} {{#withXml}} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache index 99dc720a635d..bddbf860eee3 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache @@ -12,7 +12,7 @@ @ApiModel(description = "{{{description}}}") {{/swagger1AnnotationLibrary}} {{#swagger2AnnotationLibrary}} -@Schema({{#name}}name = "{{name}}", {{/name}}description = "{{{description}}}"{{#deprecated}}, deprecated = true{{/deprecated}}) +@{{swagger2SchemaAnnotation}}({{#name}}name = "{{name}}", {{/name}}description = "{{{description}}}"{{#deprecated}}, deprecated = true{{/deprecated}}) {{/swagger2AnnotationLibrary}} {{/description}} {{#discriminator}} @@ -231,7 +231,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}} {{#lambda.trim}}{{>notNull}}{{/lambda.trim}} {{/useBeanValidation}} {{#swagger2AnnotationLibrary}} - @Schema(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = Schema.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}{{#deprecated}}, deprecated = true{{/deprecated}}, requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}{{#isNullable}}, nullable = true{{/isNullable}}) + @{{swagger2SchemaAnnotation}}(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = {{swagger2SchemaAnnotation}}.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}{{#deprecated}}, deprecated = true{{/deprecated}}, requiredMode = {{#required}}{{swagger2SchemaAnnotation}}.RequiredMode.REQUIRED{{/required}}{{^required}}{{swagger2SchemaAnnotation}}.RequiredMode.NOT_REQUIRED{{/required}}{{#isNullable}}, nullable = true{{/isNullable}}) {{/swagger2AnnotationLibrary}} {{#swagger1AnnotationLibrary}} @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}") diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 0bd582e30076..797dbd3f6210 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -2286,6 +2286,56 @@ public void testTypeMappings() { Assert.assertEquals(codegen.typeMapping().get("file"), "org.springframework.core.io.Resource"); } + @Test + public void schemaNamedSchemaDoesNotCollideWithSwagger2Annotation_issue16584() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/bugs/issue_16584.yaml", null, new ParseOptions()).getOpenAPI(); + + SpringCodegen codegen = new SpringCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(INTERFACE_ONLY, "true"); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGenerateMetadata(false); + generator.opts(input).generate(); + + // The api must reference the generated model, not the swagger annotation. Importing both + // would be a compile error: two single-type imports for the same simple name. + JavaFileAssert.assertThat(Paths.get(outputPath + "/src/main/java/org/openapitools/api/SchemasApi.java")) + .hasImports("org.openapitools.model.Schema") + .hasNoImports("io.swagger.v3.oas.annotations.media.Schema") + .fileContains("ResponseEntity getSchema") + .fileContains("@io.swagger.v3.oas.annotations.media.Schema(implementation = Schema.class)"); + + // In the model the declared class shadows the annotation, so the annotation is qualified. + JavaFileAssert.assertThat(Paths.get(outputPath + "/src/main/java/org/openapitools/model/Schema.java")) + .hasNoImports("io.swagger.v3.oas.annotations.media.Schema") + .fileContains("@io.swagger.v3.oas.annotations.media.Schema(name = \"id\""); + } + + @Test + public void schemaNamedSchemaKeepsExplicitImportMapping_issue16584() { + final SpringCodegen codegen = new SpringCodegen(); + codegen.processOpts(); + // DefaultGenerator applies --import-mappings after processOpts(), so mirror that order. + codegen.importMapping().put("Schema", "com.example.custom.Schema"); + + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/bugs/issue_16584.yaml", null, new ParseOptions()).getOpenAPI(); + codegen.preprocessOpenAPI(openAPI); + + // A user-supplied --import-mappings entry must survive the collision workaround. + Assert.assertEquals(codegen.importMapping().get("Schema"), "com.example.custom.Schema"); + } + @Test public void testImportMappings() { final SpringCodegen codegen = new SpringCodegen(); @@ -6764,9 +6814,12 @@ public void annotationLibraryDoesNotCauseImportConflictsInSpringWithAnnotationLi File apiFile = files.get("Schema.java"); assertNotNull(apiFile); - JavaFileAssert.assertThat(apiFile).fileContains( - "import io.swagger.v3.oas.annotations.media.Schema;" - ); + // The spec names a schema "Schema". A single-type import of the annotation cannot coexist + // with the model class of the same name declared in this compilation unit (JLS 7.5.1), so + // the annotation is emitted fully qualified instead of imported. See issue #16584. + JavaFileAssert.assertThat(apiFile) + .fileDoesNotContain("import io.swagger.v3.oas.annotations.media.Schema;") + .fileContains("@io.swagger.v3.oas.annotations.media.Schema("); } @Test diff --git a/modules/openapi-generator/src/test/resources/bugs/issue_16584.yaml b/modules/openapi-generator/src/test/resources/bugs/issue_16584.yaml new file mode 100644 index 000000000000..18b247fc6142 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/bugs/issue_16584.yaml @@ -0,0 +1,35 @@ +openapi: 3.0.3 +info: + title: Schema name collision + description: A schema named "Schema" collides with io.swagger.v3.oas.annotations.media.Schema. + version: 1.0.0 +paths: + /schemas/{id}: + get: + operationId: getSchema + tags: + - schemas + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: Schema found. + content: + application/json: + schema: + $ref: '#/components/schemas/Schema' +components: + schemas: + Schema: + type: object + title: Schema + properties: + id: + type: string + readOnly: true + name: + type: string