diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 70f4f50fc394..a7983e1d0490 100755 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -14,5 +14,5 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.8/apache-maven-3.8.8-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 006bdc827c08..1b797a1a40b5 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -3690,8 +3690,18 @@ protected Schema getDiscriminatorSchema(Schema schema, String discriminatorName) * @param discriminatorPropertyName The name of the discriminator property. */ protected String getDiscriminatorPropertyType(Schema schema, String discriminatorPropertyName) { - return DiscriminatorUtils.getDiscriminatorPropertyType(schema, discriminatorPropertyName) - .map(this::toModelName) + return DiscriminatorUtils.recursiveGetDiscriminatorPropertySchema(openAPI, getLegacyDiscriminatorBehavior(), schema, discriminatorPropertyName) + .map(discSchema -> { + if (discSchema.get$ref() != null) { + // $ref to a named model (e.g. a top-level enum): use the model name. + return toModelName(ModelUtils.getSimpleRef(discSchema.get$ref())); + } + // Inline schema (inline enum, uri, ...): resolve via the same pipeline the + // concrete model classes use so the oneOf interface getter type matches. + // Keep String for typeless const (isAnyType) to avoid resolving to Object. + CodegenProperty cp = fromProperty(discriminatorPropertyName, discSchema); + return (cp.isAnyType || cp.datatypeWithEnum == null) ? typeMapping.get("string") : cp.datatypeWithEnum; + }) .orElseGet(() -> typeMapping.get("string")); } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java index 5ed6f5b0761c..2bbad60bdabc 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/DiscriminatorUtils.java @@ -49,6 +49,67 @@ public static Schema getDiscriminatorSchema(Schema schema, String discriminatorN return discSchema; } + /** + * Recursively resolve the schema of the discriminator property, descending into composed + * (allOf/oneOf/anyOf) schemas and following $refs. Mirrors the traversal of + * {@link #recursiveGetDiscriminator} but returns the property {@link Schema} rather than the + * {@link Discriminator} object, so the two recursive lookups share a single pass through the + * schema hierarchy. + * + * @param openAPI The OpenAPI specification (used to dereference $refs). + * @param legacyDiscriminatorBehavior Whether legacy discriminator behaviour is enabled. + * @param schema The schema that may (transitively) declare the discriminator property. + * @param discPropName The name of the discriminator property. + * @param visitedSchemas Schemas already visited, to guard against cycles. + * @return the discriminator property schema, or {@code null} if it cannot be resolved. + */ + public static Optional recursiveGetDiscriminatorPropertySchema(OpenAPI openAPI, boolean legacyDiscriminatorBehavior, Schema schema, String discPropName) { + return Optional.ofNullable(recursiveGetDiscriminatorPropertySchema(openAPI, legacyDiscriminatorBehavior, schema, discPropName, new ArrayList<>())); + } + + private static Schema recursiveGetDiscriminatorPropertySchema( + OpenAPI openAPI, + boolean legacyDiscriminatorBehavior, + Schema schema, + String discPropName, + ArrayList visitedSchemas) { + Schema refSchema = ModelUtils.getReferencedSchema(openAPI, schema); + Schema propSchema = getDiscriminatorSchema(refSchema, discPropName); + if (propSchema != null) { + return propSchema; + } + if (legacyDiscriminatorBehavior) { + return null; + } + for (Schema s : visitedSchemas) { + if (s == refSchema) { + return null; + } + } + visitedSchemas.add(refSchema); + if (ModelUtils.isComposedSchema(refSchema)) { + if (refSchema.getAllOf() != null) { + for (Object allOf : refSchema.getAllOf()) { + Schema found = recursiveGetDiscriminatorPropertySchema(openAPI, legacyDiscriminatorBehavior, (Schema) allOf, discPropName, visitedSchemas); + if (found != null) return found; + } + } + if (ModelUtils.hasOneOf(refSchema)) { + for (Object oneOf : refSchema.getOneOf()) { + Schema found = recursiveGetDiscriminatorPropertySchema(openAPI, legacyDiscriminatorBehavior, (Schema) oneOf, discPropName, visitedSchemas); + if (found != null) return found; + } + } + if (ModelUtils.hasAnyOf(refSchema)) { + for (Object anyOf : refSchema.getAnyOf()) { + Schema found = recursiveGetDiscriminatorPropertySchema(openAPI, legacyDiscriminatorBehavior, (Schema) anyOf, discPropName, visitedSchemas); + if (found != null) return found; + } + } + } + return null; + } + /** * Recursively look in Schema sc for the discriminator and return it * diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index dc5e1378ee6e..9225c5e73bb3 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -2000,6 +2000,37 @@ public void testDiscriminatorWithMappingIssue14731() { .content().doesNotContain("@JsonTypeName"); } + @Test + public void testOneOfInterfaceInheritedEnumDiscriminator() { + final Path output = newTempFolder(); + final OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/oneOfDiscriminator.yaml", null, new ParseOptions()) + .getOpenAPI(); + + // resttemplate (unlike the default okhttp-gson) renders oneOf as interfaces, so it uses the + // base Java/oneof_interface template that emits the discriminator getter type. + final JavaClientCodegen codegen = new JavaClientCodegen(); + codegen.setLibrary("resttemplate"); + codegen.setOutputDir(output.toString()); + codegen.setUseOneOfInterfaces(true); + codegen.setLegacyDiscriminatorBehavior(false); + + final ClientOptInput input = new ClientOptInput().openAPI(openAPI).config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + generator.opts(input).generate(); + + // Issue #22541: the inline-enum discriminator inherited from a base schema via allOf must + // resolve to the enum type in the oneOf interface getter, not String. + assertThat(output.resolve("src/main/java/org/openapitools/client/model/PetResponseEnumDisc.java")) + .content().contains("public PetTypeEnum getPetType();"); + } + @Test public void testDiscriminatorWithoutMappingIssue14731() { final Path output = newTempFolder(); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/helidon/JavaHelidonCommonCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/helidon/JavaHelidonCommonCodegenTest.java index 4acb8b740b47..27d5f834db26 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/helidon/JavaHelidonCommonCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/helidon/JavaHelidonCommonCodegenTest.java @@ -21,6 +21,7 @@ import org.openapitools.codegen.DefaultGenerator; import org.openapitools.codegen.TestUtils; import org.openapitools.codegen.config.CodegenConfigurator; +import org.openapitools.codegen.languages.AbstractJavaCodegen; import org.openapitools.codegen.languages.JavaHelidonCommonCodegen; import org.testng.Assert; import org.testng.annotations.BeforeMethod; @@ -70,6 +71,35 @@ public void setup() throws IOException { mpClientGenerator = new DefaultGenerator(); } + @Test + public void testOneOfInterfaceInheritedEnumDiscriminator() throws IOException { + File output = Files.createTempDirectory("testHelidonEnumDisc").toFile(); + output.deleteOnExit(); + String outputDir = output.getAbsolutePath().replace('\\', '/'); + + CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("java-helidon-client") + .setLibrary("mp") + .setInputSpec("src/test/resources/3_0/oneOfDiscriminator.yaml") + .addAdditionalProperty(AbstractJavaCodegen.USE_ONE_OF_INTERFACES, true) + .addAdditionalProperty(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, false) + .setOutputDir(outputDir); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.opts(configurator.toClientOptInput()).generate(); + + // Issue #22541: the inline-enum discriminator inherited from a base schema via allOf must + // resolve to the enum type in the oneOf interface getter, not String. + TestUtils.assertFileContains( + Paths.get(outputDir + "/src/main/java/org/openapitools/client/model/PetResponseEnumDisc.java"), + "PetTypeEnum getPetType()"); + } + @Test public void defaultVersionTest() { runServerVersionTest(null, null); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautClientCodegenTest.java index ec5508d74db6..fda56748077a 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/micronaut/JavaMicronautClientCodegenTest.java @@ -47,6 +47,21 @@ public void testInitialConfigValues() throws Exception { configAssert.assertValue(CodegenConstants.INVOKER_PACKAGE, codegen::getInvokerPackage, "org.openapitools"); } + @Test + public void testOneOfInterfaceInheritedEnumDiscriminator() { + JavaMicronautClientCodegen codegen = new JavaMicronautClientCodegen(); + codegen.setUseOneOfInterfaces(true); + codegen.setLegacyDiscriminatorBehavior(false); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/oneOfDiscriminator.yaml", + CodegenConstants.MODELS); + + // Issue #22541: the inline-enum discriminator is inherited from a base schema via allOf, so + // the oneOf interface getter must resolve to the enum type, not String + // (DefaultCodegen.getDiscriminatorPropertyType). + assertFileContains(outputPath + "src/main/java/org/openapitools/model/PetResponseEnumDisc.java", + "public PetTypeEnum getPetType();"); + } + @Test public void testApiAndModelFilesPresent() { JavaMicronautClientCodegen codegen = new JavaMicronautClientCodegen(); 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 e7a4f02f7f0b..fac5e989aa25 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 @@ -2139,6 +2139,92 @@ public void testDiscriminatorWithoutMappingIssue14731() throws IOException { assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/ChildWithoutMappingBDTO.java"), "@JsonTypeName"); } + @Test + void testOneOfWithInheritedEnumDiscriminator() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/oneOfDiscriminator.yaml", null, new ParseOptions()).getOpenAPI(); + + SpringCodegen codegen = new SpringCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(CXFServerFeatures.LOAD_TEST_DATA_FROM_FILE, "true"); + codegen.setUseOneOfInterfaces(true); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + codegen.setHateoas(true); + generator.setGenerateMetadata(false); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "false"); + + codegen.setUseOneOfInterfaces(true); + codegen.setLegacyDiscriminatorBehavior(false); + + generator.opts(input).generate(); + + // The discriminator (inline enum) is inherited from the base PetEnumDisc via allOf. The + // oneOf interface getter must use the same enum type as the concrete base class, not + // String, otherwise the generated code does not compile (issue #22541). + assertFileContains( + Paths.get(outputPath + "/src/main/java/org/openapitools/model/PetResponseEnumDisc.java"), + "public PetTypeEnum getPetType();" + ); + assertFileContains( + Paths.get(outputPath + "/src/main/java/org/openapitools/model/PetEnumDisc.java"), + "public PetTypeEnum getPetType()" + ); + } + + @Test + void testOneOfWithInheritedUriDiscriminator() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/oneOfDiscriminator.yaml", null, new ParseOptions()).getOpenAPI(); + + SpringCodegen codegen = new SpringCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(CXFServerFeatures.LOAD_TEST_DATA_FROM_FILE, "true"); + codegen.setUseOneOfInterfaces(true); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + codegen.setHateoas(true); + generator.setGenerateMetadata(false); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "false"); + + codegen.setUseOneOfInterfaces(true); + codegen.setLegacyDiscriminatorBehavior(false); + + generator.opts(input).generate(); + + // The discriminator (string, format: uri) is inherited from the base PetUriDisc via allOf. + // The oneOf interface getter must use URI, matching the concrete base class, not String, + // otherwise the generated code does not compile (issue #18693). + assertFileContains( + Paths.get(outputPath + "/src/main/java/org/openapitools/model/PetResponseUriDisc.java"), + "public URI getPetType();" + ); + assertFileContains( + Paths.get(outputPath + "/src/main/java/org/openapitools/model/PetUriDisc.java"), + "public URI getPetType()" + ); + } + @Test void testOneOfWithEnumDiscriminator() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); 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 fb53bafa3ad0..1ea6e8443041 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 @@ -121,6 +121,39 @@ public void testNoRequestMappingAnnotation_spring_cloud_default() throws IOExcep ); } + @Test + public void testOneOfInterfaceInheritedEnumDiscriminator() throws IOException { + // Cross-generator check for the DefaultCodegen discriminator-type fix: the kotlin-spring + // oneof_interface template emits the discriminator getter type too. Issue #22541: the + // inline-enum discriminator is inherited from a base schema via allOf, so the sealed + // interface must use the enum type rather than String. + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.setUseOneOfInterfaces(true); + codegen.setLegacyDiscriminatorBehavior(false); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGenerateMetadata(false); + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "false"); + + generator.opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_0/oneOfDiscriminator.yaml")) + .config(codegen)) + .generate(); + + assertFileContains( + Paths.get(output + "/src/main/kotlin/org/openapitools/model/PetResponseEnumDisc.kt"), + "val petType: PetType" + ); + } + @Test public void testNoRequestMappingAnnotationNone() throws IOException { File output = generatePetstoreWithRequestMappingMode(KotlinSpringServerCodegen.RequestMappingMode.none); diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOfDiscriminator.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOfDiscriminator.yaml index 49bcc3ab60d3..088540b2eedd 100644 --- a/modules/openapi-generator/src/test/resources/3_0/oneOfDiscriminator.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/oneOfDiscriminator.yaml @@ -298,3 +298,61 @@ components: - $ref: '#/components/schemas/FruitType' discriminator: propertyName: fruitType + # oneOf interface whose discriminator property is an inline enum inherited from a shared base + # schema (PetEnumDisc) via allOf. The interface getter must use the same enum type the + # concrete classes generate, not String. Ref: issue #22541. + PetResponseEnumDisc: + oneOf: + - $ref: '#/components/schemas/DogEnumDisc' + - $ref: '#/components/schemas/CatEnumDisc' + PetEnumDisc: + type: object + required: + - petType + properties: + petType: + type: string + enum: + - dog + - cat + discriminator: + propertyName: petType + mapping: + dog: DogEnumDisc + cat: CatEnumDisc + DogEnumDisc: + type: object + allOf: + - $ref: '#/components/schemas/PetEnumDisc' + CatEnumDisc: + type: object + allOf: + - $ref: '#/components/schemas/PetEnumDisc' + # oneOf interface whose discriminator property is an inline string with a non-string format + # (uri) inherited from a shared base schema (PetUriDisc) via allOf. The interface getter must + # use URI, matching the concrete classes, not String. Ref: issue #18693. + PetResponseUriDisc: + oneOf: + - $ref: '#/components/schemas/DogUriDisc' + - $ref: '#/components/schemas/CatUriDisc' + PetUriDisc: + type: object + required: + - petType + properties: + petType: + type: string + format: uri + discriminator: + propertyName: petType + mapping: + dog: DogUriDisc + cat: CatUriDisc + DogUriDisc: + type: object + allOf: + - $ref: '#/components/schemas/PetUriDisc' + CatUriDisc: + type: object + allOf: + - $ref: '#/components/schemas/PetUriDisc'