From 256d4e5181120df0d7955c82b4cfea816daf406e Mon Sep 17 00:00:00 2001 From: Kejia Liu Date: Sat, 1 Aug 2026 14:02:01 +0800 Subject: [PATCH] fix(spring): avoid duplicate Tag import when a model is named Tag If the spec defines a model named `Tag` and an api uses it, the generated api imported both the model and io.swagger.v3.oas.annotations.tags.Tag. Two single-type-imports of the same simple name is a hard javac error, so the generated code did not compile: import org.openapitools.model.Tag; import io.swagger.v3.oas.annotations.tags.Tag; @Tag(name = "pets", ...) public interface TagApi { default ResponseEntity getTag(...) error: a type with the same simple name is already defined by the single-type-import of Tag The model import is the one that matters -- it is the type used in the signatures -- so reference the annotation by its fully qualified name and skip importing it. The detection is per api file, driven by that file's own imports rather than by "the spec contains a model named Tag": the petstore sample defines a Tag model but most of its api files never import it, and qualifying the annotation there would be needless churn (it rewrote 171 sample files and broke two existing SpringCodegenTest cases before being scoped down). Regenerating all 787 sample configs produces no diff. --- .../codegen/languages/SpringCodegen.java | 10 +++++++ .../main/resources/JavaSpring/api.mustache | 4 ++- .../JavaSpring/apiController.mustache | 2 ++ .../java/spring/SpringCodegenTest.java | 29 +++++++++++++++++++ .../resources/3_0/spring/model-named-tag.yaml | 25 ++++++++++++++++ 5 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/spring/model-named-tag.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 1ab10fd19d09..4ab57fff15fe 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 @@ -977,6 +977,16 @@ public void preprocessOpenAPI(OpenAPI openAPI) { @Override public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { + // A model named `Tag` collides with io.swagger.v3.oas.annotations.tags.Tag. When this file + // imports the model, importing the annotation too is a duplicate single-type-import, which + // javac rejects outright. Reference the annotation by its fully qualified name instead and + // skip its import -- only for the files that actually hit the clash. + final String tagModelImport = modelPackage() + ".Tag"; + if (objs.getImports() != null + && objs.getImports().stream().anyMatch(imp -> tagModelImport.equals(imp.get("import")))) { + objs.put("qualifySwaggerTagAnnotation", true); + } + final OperationMap operations = objs.getOperations(); if (operations != null) { final List ops = operations.getOperation(); diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache index 77ce7fd59757..c0be360863e6 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache @@ -17,7 +17,9 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +{{^qualifySwaggerTagAnnotation}} import io.swagger.v3.oas.annotations.tags.Tag; +{{/qualifySwaggerTagAnnotation}} import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.ExampleObject; {{/swagger2AnnotationLibrary}} @@ -106,7 +108,7 @@ import {{javaxPackage}}.annotation.Generated; {{/useResponseEntity}} {{/useSpringController}} {{#swagger2AnnotationLibrary}} -@Tag(name = "{{{tagName}}}", description = {{#tagDescription}}"{{{.}}}"{{/tagDescription}}{{^tagDescription}}"the {{{tagName}}} API"{{/tagDescription}}) +{{#qualifySwaggerTagAnnotation}}@io.swagger.v3.oas.annotations.tags.Tag{{/qualifySwaggerTagAnnotation}}{{^qualifySwaggerTagAnnotation}}@Tag{{/qualifySwaggerTagAnnotation}}(name = "{{{tagName}}}", description = {{#tagDescription}}"{{{.}}}"{{/tagDescription}}{{^tagDescription}}"the {{{tagName}}} API"{{/tagDescription}}) {{/swagger2AnnotationLibrary}} {{#swagger1AnnotationLibrary}} @Api(value = "{{{tagName}}}", description = {{#tagDescription}}"{{{.}}}"{{/tagDescription}}{{^tagDescription}}"the {{{tagName}}} API"{{/tagDescription}}) diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/apiController.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/apiController.mustache index 7de0ac763bcf..0ebab4e5cfb8 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/apiController.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/apiController.mustache @@ -11,7 +11,9 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +{{^qualifySwaggerTagAnnotation}} import io.swagger.v3.oas.annotations.tags.Tag; +{{/qualifySwaggerTagAnnotation}} {{/swagger2AnnotationLibrary}} {{#swagger1AnnotationLibrary}} import io.swagger.annotations.*; 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 2193ee5c9f9f..ce51c20830ee 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 @@ -1206,6 +1206,35 @@ public void shouldGenerateExclusiveMinMaxForOAS31() throws IOException { } + @Test + public void modelNamedTagDoesNotClashWithSwaggerTagAnnotation() throws IOException { + // A model named `Tag` collides with io.swagger.v3.oas.annotations.tags.Tag. Importing both + // is a duplicate single-type-import, which javac rejects outright, so the generated api + // must fall back to the fully qualified annotation name and skip the import. + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/spring/model-named-tag.yaml", null, new ParseOptions()) + .getOpenAPI(); + SpringCodegen codegen = new SpringCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(INTERFACE_ONLY, "true"); + + ClientOptInput input = new ClientOptInput().openAPI(openAPI).config(codegen); + DefaultGenerator generator = new DefaultGenerator(); + generator.setGenerateMetadata(false); + + Map files = generator.opts(input).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + + Path api = files.get("TagApi.java").toPath(); + assertFileNotContains(api, "import io.swagger.v3.oas.annotations.tags.Tag;"); + assertFileContains(api, "@io.swagger.v3.oas.annotations.tags.Tag(name = \"pets\""); + // the model import must still be there -- that is the type actually used in signatures + assertFileContains(api, "import org.openapitools.model.Tag;"); + } + @Test public void shouldUseTagsForClassname() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/model-named-tag.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/model-named-tag.yaml new file mode 100644 index 000000000000..d7e34d7f834f --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/spring/model-named-tag.yaml @@ -0,0 +1,25 @@ +openapi: 3.0.3 +info: + title: model named Tag + version: 1.0.0 +tags: + - name: pets +paths: + /tag: + get: + tags: + - pets + operationId: getTag + responses: + "200": + description: ok + content: + application/json: + schema: + $ref: "#/components/schemas/Tag" +components: + schemas: + Tag: + properties: + id: + type: string