From dd4ea86a52baee46e04a209132a75deebcf6ba79 Mon Sep 17 00:00:00 2001 From: Mattias-Sehlstedt <60173714+Mattias-Sehlstedt@users.noreply.github.com> Date: Sat, 27 Jun 2026 19:46:04 +0200 Subject: [PATCH] [core] Add introspection support for discovering the discriminator type in a oneOf allOf polymorphic structure --- .../openapitools/codegen/DefaultCodegen.java | 31 ++++++- .../codegen/utils/DiscriminatorUtils.java | 65 ++++++++++++-- .../codegen/utils/ModelUtils.java | 10 +++ .../utils/OneOfImplementorAdditionalData.java | 28 +++--- .../codegen/DefaultCodegenTest.java | 81 +++++++++++++++++ .../codegen/java/JavaClientCodegenTest.java | 31 +++++++ .../helidon/JavaHelidonCommonCodegenTest.java | 30 +++++++ .../JavaMicronautClientCodegenTest.java | 15 ++++ .../java/spring/SpringCodegenTest.java | 86 ++++++++++++++++++ .../spring/KotlinSpringServerCodegenTest.java | 33 +++++++ .../resources/3_0/oneOfDiscriminator.yaml | 87 +++++++++++++++++++ 11 files changed, 470 insertions(+), 27 deletions(-) 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..5c2f52a2f6f8 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 @@ -93,6 +93,7 @@ import static org.openapitools.codegen.CodegenConstants.*; import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER; +import static org.openapitools.codegen.utils.DiscriminatorUtils.*; import static org.openapitools.codegen.utils.OnceLogger.once; import static org.openapitools.codegen.utils.StringUtils.*; @@ -1720,7 +1721,11 @@ public String toArrayModelParamName(String name) { */ @SuppressWarnings("static-method") public String toEnumName(CodegenProperty property) { - return StringUtils.capitalize(property.name) + "Enum"; + return toEnumName(property.name); + } + + public String toEnumName(String propertyName) { + return StringUtils.capitalize(propertyName) + "Enum"; } /** @@ -3453,7 +3458,7 @@ private CodegenProperty discriminatorFound(String composedSchemaName, Schema sc, * @param sc The Schema that may contain the discriminator * @param visitedSchemas An array list of visited schemas */ - private Discriminator recursiveGetDiscriminator(Schema sc, ArrayList visitedSchemas) { + private DiscriminatorData recursiveGetDiscriminator(Schema sc, ArrayList visitedSchemas) { return DiscriminatorUtils.recursiveGetDiscriminator(openAPI, this.getLegacyDiscriminatorBehavior(), sc, visitedSchemas); } @@ -3590,7 +3595,7 @@ protected List getAllOfDescendants(String thisSchemaName) { } protected CodegenDiscriminator createDiscriminator(String schemaName, Schema schema) { - Discriminator sourceDiscriminator = recursiveGetDiscriminator(schema, new ArrayList()); + DiscriminatorData sourceDiscriminator = recursiveGetDiscriminator(schema, new ArrayList()); if (sourceDiscriminator == null) { return null; } @@ -3607,7 +3612,25 @@ protected CodegenDiscriminator createDiscriminator(String schemaName, Schema sch // FIXME: there are other ways to define the type of the discriminator property (inline // for example). Handling those scenarios is too complicated for me, I'm leaving it for // the future.. - discriminator.setPropertyType(getDiscriminatorPropertyType(schema, discriminatorPropertyName)); + String discriminatorType; + Schema discSchema = sourceDiscriminator.getDiscriminatorSchema(); + if (discSchema != null) { + if (ModelUtils.hasRef(discSchema)) { + discriminatorType = toModelName(ModelUtils.getSimpleRef(discSchema.get$ref())); + } else { + // 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 to avoid resolving to Object. + if (ModelUtils.getType(discSchema) == null || discSchema.getEnum() == null) { + discriminatorType = getTypeDeclaration(discSchema); + } else { + discriminatorType = toEnumName(discriminatorPropertyName); + } + } + } else { + discriminatorType = getDiscriminatorPropertyType(schema, discriminatorPropertyName); + } + discriminator.setPropertyType(discriminatorType); // check to see if the discriminator property is an enum string boolean isEnum = Optional 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..eb4ee86a8b4b 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 @@ -3,6 +3,7 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.Discriminator; import io.swagger.v3.oas.models.media.Schema; +import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,6 +17,8 @@ public class DiscriminatorUtils { private static final String CONFLICTING_DISCRIMINATOR_NAMES = "The alternative schemas have conflicting discriminator property names. The schemas must have the same property name, but found {}"; + private static final String CONFLICTING_DISCRIMINATOR_TYPES = + "The alternative schemas have conflicting discriminator property types ({})"; /** * Gets the simple ref name of the discriminator property type from the schema. @@ -35,7 +38,7 @@ public static Optional getDiscriminatorPropertyType(Schema schema, Strin * An example of a sibling is an enum-ref that has its own description. This will lead to the enum being * referenced as an allOf that in turn has a ref, rather than a regular ref directly to the enum. * - * @param schema The input OAS schema. + * @param schema The input OAS schema that has the discriminator as a property. * @param discriminatorName The name of the discriminator property. */ public static Schema getDiscriminatorSchema(Schema schema, String discriminatorName) { @@ -57,15 +60,19 @@ public static Schema getDiscriminatorSchema(Schema schema, String discriminatorN * @param sc The Schema that may contain the discriminator * @param visitedSchemas an array list of visited schemas */ - public static Discriminator recursiveGetDiscriminator( + public static DiscriminatorData recursiveGetDiscriminator( OpenAPI openAPI, boolean legacyDiscriminatorBehavior, Schema sc, ArrayList visitedSchemas) { Schema refSchema = ModelUtils.getReferencedSchema(openAPI, sc); - Discriminator foundDisc = refSchema.getDiscriminator(); - if (foundDisc != null) { - return foundDisc; + DiscriminatorData foundDisc = new DiscriminatorData(refSchema.getDiscriminator(), null); + if (foundDisc.getDiscriminator() != null) { + String discriminatorPropertyName = foundDisc.getDiscriminator().getPropertyName(); + return new DiscriminatorData( + foundDisc.getDiscriminator(), + DiscriminatorUtils.getDiscriminatorSchema(refSchema, discriminatorPropertyName) + ); } if (legacyDiscriminatorBehavior) { @@ -89,7 +96,7 @@ public static Discriminator recursiveGetDiscriminator( if (foundDisc != null) { disc.setPropertyName(foundDisc.getPropertyName()); disc.setMapping(foundDisc.getMapping()); - return disc; + return new DiscriminatorData(disc, foundDisc.getDiscriminatorSchema()); } } } @@ -115,16 +122,17 @@ public static Discriminator recursiveGetDiscriminator( * @param visitedSchemas an array list of visited schemas * @return the discriminator if the alternatives correctly shares one, otherwise null */ - private static Discriminator getDiscriminatorFromAlternatives( + private static DiscriminatorData getDiscriminatorFromAlternatives( OpenAPI openAPI, boolean legacyDiscriminatorBehavior, List alternativeSchemas, ArrayList visitedSchemas) { Discriminator discriminator = new Discriminator(); - Discriminator foundDisc = null; + DiscriminatorData foundDisc = null; Integer hasDiscriminatorCnt = 0; Integer hasNullTypeCnt = 0; Set discriminatorsPropNames = new HashSet<>(); + Set discriminatorTypes = new HashSet<>(); for (Object alternative : alternativeSchemas) { if (ModelUtils.isNullType((Schema) alternative)) { // The null type does not have a discriminator. Skip. @@ -135,20 +143,59 @@ private static Discriminator getDiscriminatorFromAlternatives( if (foundDisc != null) { discriminatorsPropNames.add(foundDisc.getPropertyName()); hasDiscriminatorCnt++; + if (foundDisc.getDiscriminatorSchema() != null) { + discriminatorTypes.add(foundDisc.getDiscriminatorSchema()); + } } } if (discriminatorsPropNames.size() > 1) { once(LOGGER).warn(CONFLICTING_DISCRIMINATOR_NAMES, String.join(", ", discriminatorsPropNames)); } + if (discriminatorTypes.size() > 1) { + once(LOGGER).warn(CONFLICTING_DISCRIMINATOR_TYPES, String.join(", ", discriminatorTypes.toString())); + } boolean allAlternativesHaveADiscriminator = hasDiscriminatorCnt + hasNullTypeCnt == alternativeSchemas.size(); if (foundDisc != null && allAlternativesHaveADiscriminator && discriminatorsPropNames.size() == 1) { discriminator.setPropertyName(foundDisc.getPropertyName()); discriminator.setMapping(foundDisc.getMapping()); - return discriminator; + Schema uniqueDiscriminatorType = discriminatorTypes.size() == 1 ? discriminatorTypes.iterator().next() : null; + return new DiscriminatorData(discriminator, uniqueDiscriminatorType); } // If the scenario when composite schema has two children and one of them is the 'null' type, // there is no need for a discriminator. return null; } + public static class DiscriminatorData { + private final Discriminator discriminator; + + @Nullable + private final Schema discriminatorSchema; + + public DiscriminatorData(Discriminator discriminator, @Nullable Schema discriminatorSchema) { + this.discriminator = discriminator; + this.discriminatorSchema = discriminatorSchema; + } + + public Discriminator getDiscriminator() { + return discriminator; + } + + public @Nullable Schema getDiscriminatorSchema() { + return discriminatorSchema; + } + + public String getPropertyName() { + return discriminator.getPropertyName(); + } + + public Map getMapping() { + return discriminator.getMapping(); + } + + public Map getExtensions() { + return discriminator.getExtensions(); + } + } + } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index a1af2787e012..e7027822a51f 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -2392,6 +2392,16 @@ public static boolean hasAnyOf(Schema schema) { return false; } + /** + * Returns true if the schema contains a $ref + * + * @param schema the schema + * @return true if $ref is set + */ + public static boolean hasRef(Schema schema) { + return schema != null && schema.get$ref() != null; + } + /** * Returns schema type. * For 3.1 spec, return the first one. diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalData.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalData.java index 395c7229bb99..b51528d4c58f 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalData.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/OneOfImplementorAdditionalData.java @@ -15,21 +15,21 @@ * This class holds data to add to `oneOf` members. Let's consider this example: *

* Foo: - * properties: - * x: - * oneOf: - * - $ref: "#/components/schemas/One - * - $ref: "#/components/schemas/Two - * y: - * type: string + * properties: + * x: + * oneOf: + * - $ref: "#/components/schemas/One" + * - $ref: "#/components/schemas/Two" + * y: + * type: string * One: - * properties: - * z: - * type: string + * properties: + * z: + * type: string * Two: - * properties: - * a: - * type: string + * properties: + * a: + * type: string *

* In codegens that use this mechanism, `Foo` will become an interface and `One` will * become its implementing class. This class carries all data necessary to properly modify @@ -99,7 +99,7 @@ public void addFromInterfaceModel(CodegenModel cm, List> mod * @param cc CodegenConfig running this operation * @param implcm the implementing model * @param implImports imports of the implementing model - * @param addInterfaceImports whether or not to add the interface model as import (will vary by language) + * @param addInterfaceImports whether to add the interface model as import (will vary by language) */ @SuppressWarnings("unchecked") public void addToImplementor(CodegenConfig cc, CodegenModel implcm, List> implImports, boolean addInterfaceImports) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index f46f712a3890..7e12b85fd108 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -1210,6 +1210,87 @@ public void testEnumDiscriminatorWithDescriptionOverridden3_1() { assertTrue(fruitModel.getVars().get(0).isEnumRef); } + @Test + public void testOneOfAllOfEnumRefDiscriminatorInheritance() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneOfDiscriminator.yaml"); + new OpenAPINormalizer(openAPI, Map.of()).normalize(); + DefaultCodegen codegen = new DefaultCodegen(); + codegen.setUseOneOfInterfaces(true); + codegen.setLegacyDiscriminatorBehavior(false); + + Schema inner = openAPI.getComponents().getSchemas().get("VehicleResponse"); + codegen.setOpenAPI(openAPI); + CodegenModel innerModel = codegen.fromModel("VehicleResponse", inner); + assertTrue(innerModel.getHasDiscriminatorWithNonEmptyMapping()); + assertTrue(innerModel.discriminator.getIsEnum()); + assertEquals("VehicleType", innerModel.discriminator.getPropertyType()); + assertTrue(innerModel.getVars().get(0).isEnumRef); + + Schema car = openAPI.getComponents().getSchemas().get("Car"); + CodegenModel carModel = codegen.fromModel("Car", car); + assertTrue(carModel.discriminator.getIsEnum()); + assertEquals("VehicleType", carModel.discriminator.getPropertyType()); + assertTrue(carModel.getVars().get(0).isEnumRef); + + Schema bike = openAPI.getComponents().getSchemas().get("Bike"); + CodegenModel bikeModel = codegen.fromModel("Bike", bike); + assertTrue(bikeModel.discriminator.getIsEnum()); + assertEquals("VehicleType", bikeModel.discriminator.getPropertyType()); + assertTrue(bikeModel.getVars().get(0).isEnumRef); + } + + @Test + public void testOneOfAllOfInlineEnumDiscriminatorInheritance() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneOfDiscriminator.yaml"); + new OpenAPINormalizer(openAPI, Map.of()).normalize(); + DefaultCodegen codegen = new DefaultCodegen(); + codegen.setUseOneOfInterfaces(true); + codegen.setLegacyDiscriminatorBehavior(false); + + Schema inner = openAPI.getComponents().getSchemas().get("PetResponseEnumDisc"); + codegen.setOpenAPI(openAPI); + CodegenModel innerModel = codegen.fromModel("PetResponseEnumDisc", inner); + assertTrue(innerModel.getHasDiscriminatorWithNonEmptyMapping()); + assertTrue(innerModel.discriminator.getIsEnum()); + assertEquals("PetTypeEnum", innerModel.discriminator.getPropertyType()); + assertFalse(innerModel.getVars().get(0).isEnumRef); + + Schema dog = openAPI.getComponents().getSchemas().get("DogEnumDisc"); + CodegenModel dogModel = codegen.fromModel("DogEnumDisc", dog); + assertTrue(dogModel.discriminator.getIsEnum()); + assertEquals("PetTypeEnum", dogModel.discriminator.getPropertyType()); + assertFalse(dogModel.getVars().get(0).isEnumRef); + + Schema cat = openAPI.getComponents().getSchemas().get("CatEnumDisc"); + CodegenModel catModel = codegen.fromModel("CatEnumDisc", cat); + assertTrue(catModel.discriminator.getIsEnum()); + assertEquals("PetTypeEnum", catModel.discriminator.getPropertyType()); + assertFalse(catModel.getVars().get(0).isEnumRef); + } + + @Test + public void testOneOfAllOfInlineStringWithFormatDiscriminatorInheritance() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/oneOfDiscriminator.yaml"); + new OpenAPINormalizer(openAPI, Map.of()).normalize(); + DefaultCodegen codegen = new DefaultCodegen(); + codegen.setUseOneOfInterfaces(true); + codegen.setLegacyDiscriminatorBehavior(false); + + Schema inner = openAPI.getComponents().getSchemas().get("PetResponseUriDisc"); + codegen.setOpenAPI(openAPI); + CodegenModel innerModel = codegen.fromModel("PetResponseUriDisc", inner); + assertTrue(innerModel.getHasDiscriminatorWithNonEmptyMapping()); + assertEquals("URI", innerModel.discriminator.getPropertyType()); + + Schema dog = openAPI.getComponents().getSchemas().get("DogUriDisc"); + CodegenModel dogModel = codegen.fromModel("DogUriDisc", dog); + assertEquals("URI", dogModel.discriminator.getPropertyType()); + + Schema cat = openAPI.getComponents().getSchemas().get("CatUriDisc"); + CodegenModel catModel = codegen.fromModel("CatUriDisc", cat); + assertEquals("URI", catModel.discriminator.getPropertyType()); + } + @Test public void testParentName() { final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/allOf.yaml"); 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 0c256b89f3b8..3087b2375660 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 @@ -2038,6 +2038,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..03aefd590817 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,90 @@ 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' + # oneOf interface whose discriminator property is an enum inherited from through a shared base + # schema (Vehicle) via allOf -> property ref. The interface getter must use the same enum type the + # concrete classes generate, not String. Ref: issue #22541. + VehicleResponse: + oneOf: + - $ref: '#/components/schemas/Car' + - $ref: '#/components/schemas/Bike' + VehicleType: + type: string + enum: + - Car + - Bike + Vehicle: + type: object + discriminator: + propertyName: vehicleType + properties: + vehicleType: + $ref: '#/components/schemas/VehicleType' + required: + - vehicleType + Car: + type: object + allOf: + - $ref: '#/components/schemas/Vehicle' + Bike: + type: object + allOf: + - $ref: '#/components/schemas/Vehicle' \ No newline at end of file