Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Schema> 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<Schema> 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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'