From bf1dc5f44962e81ac57b66d83220700620341ffb Mon Sep 17 00:00:00 2001 From: jpfinne Date: Tue, 14 Apr 2026 17:06:18 +0200 Subject: [PATCH 01/22] normalizer REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING --- docs/customization.md | 7 +++ .../codegen/OpenAPINormalizer.java | 34 ++++++++++++++ .../codegen/OpenAPINormalizerTest.java | 28 ++++++++++++ .../codegen/java/JavaClientCodegenTest.java | 30 +++++++++++++ .../java/assertions/JavaFileAssert.java | 30 +++++++++++++ .../java/spring/SpringCodegenTest.java | 31 +++++++++++++ .../resources/3_0/spring/issue_23527.yaml | 41 +++++++++++++++++ .../resources/3_0/spring/issue_23527_1.yaml | 44 +++++++++++++++++++ .../resources/3_0/spring/issue_23527_2.yaml | 41 +++++++++++++++++ pom.xml | 11 ++++- 10 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_1.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_2.yaml diff --git a/docs/customization.md b/docs/customization.md index ba113c6705fc..16f197ec588d 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -651,6 +651,13 @@ Example: java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/required-properties.yaml -o /tmp/java-okhttp/ --openapi-normalizer REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT=true ``` +- `REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING``: when set to true, oneOf is removed and is converted into mappings in a discriminator mapping. + +Example: +``` +java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g spring -i modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml -o /tmp/java-spring/ --openapi-normalizer REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING=true +``` + - `FILTER` The `FILTER` parameter allows selective inclusion of API operations based on specific criteria. It applies the `x-internal: true` property to operations that do **not** match the specified values, preventing them from being generated. Multiple filters can be separated by a semicolon. diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 95f793c195da..dc4c60b40aa9 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -78,6 +78,10 @@ public class OpenAPINormalizer { // are removed as most generators cannot handle such case at the moment final String REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY = "REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY"; + // when set to true, oneOf is removed and is converted into mappings in a discriminator mapping + final String REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING = "REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING"; + + // when set to true, oneOf/anyOf with either string or enum string as sub schemas will be simplified // to just string final String SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING = "SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING"; @@ -214,6 +218,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map inputRules) { ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM); ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT); ruleNames.add(SORT_MODEL_PROPERTIES); + ruleNames.add(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING); // rules that are default to true rules.put(SIMPLIFY_ONEOF_ANYOF, true); @@ -1053,6 +1058,8 @@ protected Schema normalizeOneOf(Schema schema, Set visitedSchemas) { // simplify first as the schema may no longer be a oneOf after processing the rule below schema = processSimplifyOneOf(schema); + schema = processReplaceOneOfByMapping(schema); + // if it's still a oneOf, loop through the sub-schemas if (schema.getOneOf() != null) { for (int i = 0; i < schema.getOneOf().size(); i++) { @@ -1569,6 +1576,33 @@ protected Schema processSimplifyOneOf(Schema schema) { return schema; } + + protected Schema processReplaceOneOfByMapping(Schema schema) { + if (!getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) { + return schema; + } + + if (schema.getDiscriminator() != null) { + Discriminator discriminator = schema.getDiscriminator(); + if (discriminator.getMapping() == null) { + Map mapping = new TreeMap<>(); + discriminator.setMapping(mapping); + for (Object oneOfObject : schema.getOneOf()) { + Schema oneOf = (Schema) oneOfObject; + String ref = oneOf.get$ref(); + if (ref != null) { + String name = ref.contains("/") ? ref.substring(ref.lastIndexOf('/') + 1) : ref; + mapping.put(name, oneOf.get$ref()); + } + } + } + + schema.setOneOf(null); + } + return schema; + } + + /** * Set nullable to true in array/set if needed. * diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 28829fb748d8..e72447f3f20b 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -16,6 +16,9 @@ package org.openapitools.codegen; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.util.Yaml; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; @@ -1502,4 +1505,29 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { } } + @Test + public void testREPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING() { + // to test array schema processing in 3.1 spec + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/spring/issue_23527.yaml"); + + Map inputRules = Map.of("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true"); + OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, inputRules); + openAPINormalizer.normalize(); + dump(openAPI); + } + + private void dump(OpenAPI openAPI) { + + ObjectMapper mapper = Yaml.mapper(); + String yaml = null; + try { + yaml = mapper.writeValueAsString(openAPI); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + System.out.println(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 b34fce2ee042..b14e91bd0305 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 @@ -72,6 +72,7 @@ import static org.openapitools.codegen.CodegenConstants.*; import static org.openapitools.codegen.TestUtils.*; import static org.openapitools.codegen.languages.JavaClientCodegen.*; +import static org.openapitools.codegen.languages.SpringCodegen.SPRING_BOOT; import static org.testng.Assert.*; public class JavaClientCodegenTest { @@ -4325,4 +4326,33 @@ public void testJspecify(String library, boolean useSpringBoot4, boolean hasJspe .fileContains("@org.jspecify.annotations.NullMarked"); } + + @DataProvider(name = "replaceOneOf") + public Object[][] replaceOneOf() { + return new Object[][]{ + {"src/test/resources/3_0/spring/issue_23527.yaml"}, + {"src/test/resources/3_0/spring/issue_23527_1.yaml"}, + {"src/test/resources/3_0/spring/issue_23527_2.yaml"} + }; + } + + @Test(dataProvider = "replaceOneOf" ) + void replaceOneOfByDiscriminatorMapping(String file) throws IOException { + Map files = generateFromContract(file, APACHE, Map.of(), + codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); + + JavaFileAssert.assertThat(files.get("GeoJsonObject.java")) + .isNormalClass() + .doesNotExtendsClasses() + .fileContains("String type") + .fileDoesNotContain("coordinates") + .assertTypeAnnotations() + .containsWithName("JsonSubTypes"); + + JavaFileAssert.assertThat(files.get("Polygon.java")) + .extendsClass("GeoJsonObject") + .doesNotImplementInterfaces("GeoJsonObject") + .fileContains("List coordinates"); + + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java index db79fecc3128..5389a4de51a9 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java @@ -59,6 +59,36 @@ public JavaFileAssert isNormalClass() { return this; } + public JavaFileAssert extendsClass(String... parentClass) { + Set expectedClasses = Stream.of(parentClass) + .collect(Collectors.toSet()); + + Set actualParents = actual.getType(0) + .asClassOrInterfaceDeclaration().getExtendedTypes() + .stream() + .map(ClassOrInterfaceType::getNameWithScope) + .collect(Collectors.toSet()); + + Assertions.assertThat(actualParents) + .withFailMessage("Expected type %s to extends %s, but found %s", + actual.getType(0).getName().asString(), expectedClasses, actualParents) + .isEqualTo(expectedClasses); + return this; + } + + public JavaFileAssert doesNotExtendsClasses() { + Set actualParents = actual.getType(0) + .asClassOrInterfaceDeclaration().getExtendedTypes() + .stream() + .map(ClassOrInterfaceType::getNameWithScope) + .collect(Collectors.toSet()); + Assertions.assertThat(actualParents) + .withFailMessage("Expected type %s to extends a class, but found %s", + actual.getType(0).getName().asString(), actualParents) + .isEmpty(); + return this; + } + public JavaFileAssert implementsInterfaces(String... implementedInterfaces) { Set expectedInterfaces = Stream.of(implementedInterfaces) .collect(Collectors.toSet()); 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 96444d9aee77..2275ab59c5ee 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 @@ -6664,4 +6664,35 @@ public void testJspecify(String library, int springBootVersion, String fooApiFil JavaFileAssert.assertThat(files.get("model/package-info.java")) .fileContains("@org.jspecify.annotations.NullMarked"); } + + + @DataProvider(name = "replaceOneOf") + public Object[][] replaceOneOf() { + return new Object[][]{ + {"src/test/resources/3_0/spring/issue_23527.yaml"}, + {"src/test/resources/3_0/spring/issue_23527_1.yaml"}, + {"src/test/resources/3_0/spring/issue_23527_2.yaml"} + }; + } + + @Test(dataProvider = "replaceOneOf" ) + void replaceOneOfByDiscriminatorMapping(String file) throws IOException { + Map files = generateFromContract(file, SPRING_BOOT, Map.of(), + codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); + + JavaFileAssert.assertThat(files.get("GeoJsonObject.java")) + .isNormalClass() + .doesNotExtendsClasses() + .fileContains("String type") + .fileDoesNotContain("coordinates") + .assertTypeAnnotations() + .containsWithName("JsonSubTypes") + ; + + JavaFileAssert.assertThat(files.get("Polygon.java")) + .extendsClass("GeoJsonObject") + .doesNotImplementInterfaces("GeoJsonObject") + .fileContains("List coordinates"); + + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml new file mode 100644 index 000000000000..349d3ce75a7d --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml @@ -0,0 +1,41 @@ +openapi: 3.0.3 +info: + title: GeoJSON Discriminator Test + version: 1.0.0 +paths: {} +components: + schemas: + GeoJsonObject: + type: object + properties: + type: + type: string + required: + - type + discriminator: + propertyName: type + oneOf: + - $ref: '#/components/schemas/Polygon' + - $ref: '#/components/schemas/MultiPolygon' + Polygon: + allOf: + - $ref: '#/components/schemas/GeoJsonObject' + - type: object + properties: + coordinates: + type: array + items: + type: number + format: double + MultiPolygon: + allOf: + - $ref: '#/components/schemas/GeoJsonObject' + - type: object + properties: + coordinates: + type: array + items: + type: array + items: + type: number + format: double \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_1.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_1.yaml new file mode 100644 index 000000000000..d212feddcb84 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_1.yaml @@ -0,0 +1,44 @@ +openapi: 3.0.3 +info: + title: GeoJSON Discriminator Test + version: 1.0.0 +paths: {} +components: + schemas: + GeoJsonObject: + type: object + properties: + type: + type: string + required: + - type + discriminator: + propertyName: type + mapping: + Polygon: '#/components/schemas/Polygon' + MultiPolygon: '#/components/schemas/MultiPolygon' + oneOf: + - $ref: '#/components/schemas/Polygon' + - $ref: '#/components/schemas/MultiPolygon' + Polygon: + allOf: + - $ref: '#/components/schemas/GeoJsonObject' + - type: object + properties: + coordinates: + type: array + items: + type: number + format: double + MultiPolygon: + allOf: + - $ref: '#/components/schemas/GeoJsonObject' + - type: object + properties: + coordinates: + type: array + items: + type: array + items: + type: number + format: double \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_2.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_2.yaml new file mode 100644 index 000000000000..5465880c6753 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_2.yaml @@ -0,0 +1,41 @@ +openapi: 3.0.3 +info: + title: GeoJSON Discriminator Test + version: 1.0.0 +paths: {} +components: + schemas: + GeoJsonObject: + type: object + properties: + type: + type: string + required: + - type + discriminator: + propertyName: type + mapping: + Polygon: '#/components/schemas/Polygon' + MultiPolygon: '#/components/schemas/MultiPolygon' + Polygon: + allOf: + - $ref: '#/components/schemas/GeoJsonObject' + - type: object + properties: + coordinates: + type: array + items: + type: number + format: double + MultiPolygon: + allOf: + - $ref: '#/components/schemas/GeoJsonObject' + - type: object + properties: + coordinates: + type: array + items: + type: array + items: + type: number + format: double \ No newline at end of file diff --git a/pom.xml b/pom.xml index 23cbd292a01a..82f97a68c6cb 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,15 @@ scm:git:git@github.com:openapitools/openapi-generator.git https://github.com/openapitools/openapi-generator + + modules/openapi-generator-core + modules/openapi-generator + modules/openapi-generator-cli + modules/openapi-generator-maven-plugin + + + + @@ -408,7 +417,6 @@ com.gradle develocity-maven-extension - ${develocity-maven-extension.version} @@ -1235,7 +1243,6 @@ 2.20.0 3.18.0 1.10.0 - 1.23.2 1.3.0 1.0.2 4.9.10 From 18ac063313834ef33a6a3906c43987ab4f62cb0e Mon Sep 17 00:00:00 2001 From: jpfinne Date: Thu, 16 Apr 2026 16:52:15 +0200 Subject: [PATCH 02/22] Work in progress --- .../codegen/OpenAPINormalizer.java | 65 ++++++++-- .../codegen/languages/SpringCodegen.java | 6 + .../org/openapitools/codegen/TestUtils.java | 18 ++- .../codegen/java/JavaClientCodegenTest.java | 62 ++++++++- .../java/spring/SpringCodegenTest.java | 18 +++ .../test/resources/3_0/composed-oneof.yaml | 52 ++++---- .../test/resources/3_0/java/issue_19261.yaml | 79 ++++++++++++ .../test/resources/3_0/java/issue_22013.yaml | 55 ++++++++ .../test/resources/3_0/java/issue_23276.yaml | 120 ++++++++++++++++++ .../test/resources/3_0/java/issue_912.yaml | 79 ++++++++++++ .../resources/3_0/spring/issue_23527.yaml | 2 +- 11 files changed, 515 insertions(+), 41 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/java/issue_19261.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/java/issue_22013.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/java/issue_23276.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/java/issue_912.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index dc4c60b40aa9..b1003d6314a8 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -644,6 +644,10 @@ protected void normalizeComponentsSchemas() { // normalize the schemas schemas.put(schemaName, normalizeSchema(schema, new HashSet<>())); + + if (getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) { + ensureInheritance(schema, schemaName); + } } } } @@ -1577,32 +1581,75 @@ protected Schema processSimplifyOneOf(Schema schema) { } + /** + * Ensure inheritance is correctly defined for OneOf and Discriminators. + * + * For schemas containing oneOf and discriminator.propertyName: + * Create the mappings as $refs + * Remove OneOf + * + * For referenced schemas, ensure that there is an allOf with this schema. + */ protected Schema processReplaceOneOfByMapping(Schema schema) { if (!getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) { return schema; } - - if (schema.getDiscriminator() != null) { - Discriminator discriminator = schema.getDiscriminator(); + Discriminator discriminator = schema.getDiscriminator(); + if (discriminator != null) { if (discriminator.getMapping() == null) { - Map mapping = new TreeMap<>(); - discriminator.setMapping(mapping); + Map mappings = new TreeMap<>(); + discriminator.setMapping(mappings); for (Object oneOfObject : schema.getOneOf()) { Schema oneOf = (Schema) oneOfObject; - String ref = oneOf.get$ref(); - if (ref != null) { - String name = ref.contains("/") ? ref.substring(ref.lastIndexOf('/') + 1) : ref; - mapping.put(name, oneOf.get$ref()); + String refSchema = oneOf.get$ref(); + if (refSchema != null) { + String name = refSchema.contains("/") ? refSchema.substring(refSchema.lastIndexOf('/') + 1) : refSchema; + mappings.put(name, refSchema); } } } + schema.setOneOf(null); } return schema; } + protected void ensureInheritance(Schema parent, String parentName) { + Discriminator discriminator = parent.getDiscriminator(); + if (discriminator != null && discriminator.getMapping() != null) { + for (String mapping : discriminator.getMapping().keySet()) { + Schema child = ModelUtils.getSchema(openAPI, mapping); + if (child != null) { + ensureInheritance(parent, child, parentName); + } + } + } + } + + protected void ensureInheritance(Schema parent, Schema child, String parentName) { + List allOf = child.getAllOf(); + if (allOf != null) { + if (hasParent(child, parent)) { + return; + } + } else { + allOf = new ArrayList<>(); + child.setAllOf(allOf); + } + Schema refToParent = new Schema<>().$ref("#/components/schemas/" + parentName); + allOf.add(refToParent); + } + + private boolean hasParent(Schema child, Schema parent) { + List allOf = child.getAllOf(); + if (allOf != null) { + return allOf.stream().anyMatch(s -> hasParent(s, parent)); + } + return false; + } + /** * Set nullable to true in array/set if needed. * 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 e6a150c4e21a..a44a8bf1f302 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 @@ -845,10 +845,15 @@ public void preprocessOpenAPI(OpenAPI openAPI) { @Override public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { + if (isSpringCodegen()) { + Map newImport = Map.of("import", importMapping.get("Nullable")); + objs.getImports().add(newImport); + } final OperationMap operations = objs.getOperations(); if (operations != null) { final List ops = operations.getOperation(); for (final CodegenOperation operation : ops) { + final List responses = operation.responses; if (responses != null) { for (final CodegenResponse resp : responses) { @@ -894,6 +899,7 @@ public void setIsVoid(boolean isVoid) { prepareVersioningParameters(ops); handleImplicitHeaders(operation); + } // The tag for the controller is the first tag of the first operation final CodegenOperation firstOperation = ops.get(0); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/TestUtils.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/TestUtils.java index a6df487ef840..fbf55bd66de0 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/TestUtils.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/TestUtils.java @@ -15,6 +15,7 @@ import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.servers.Server; import io.swagger.v3.parser.core.models.ParseOptions; +import org.apache.commons.io.FileUtils; import org.openapitools.codegen.java.assertions.JavaFileAssert; import org.openapitools.codegen.model.ModelMap; import org.openapitools.codegen.model.ModelsMap; @@ -342,14 +343,23 @@ public static ModelsMap createCodegenModelWrapper(CodegenModel cm) { } public static Path newTempFolder() { - final Path tempDir; + File file = new File("c:\\temp\\generated"); + file.mkdir(); try { - tempDir = Files.createTempDirectory("test"); + FileUtils.cleanDirectory(file); } catch (IOException e) { throw new RuntimeException(e); } - tempDir.toFile().deleteOnExit(); - return tempDir; + return file.toPath(); +// final Path tempDir; +// try { +// tempDir = Files.createTempDirectory("test"); +// } catch (IOException e) { +// throw new RuntimeException(e); +// } +// tempDir.toFile().deleteOnExit(); +// +// return tempDir; } } 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 b14e91bd0305..6a825721357e 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 @@ -4337,7 +4337,7 @@ public Object[][] replaceOneOf() { } @Test(dataProvider = "replaceOneOf" ) - void replaceOneOfByDiscriminatorMapping(String file) throws IOException { + void replaceOneOfByDiscriminatorMapping(String file) { Map files = generateFromContract(file, APACHE, Map.of(), codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); @@ -4355,4 +4355,64 @@ void replaceOneOfByDiscriminatorMapping(String file) throws IOException { .fileContains("List coordinates"); } + @Test + void issue_23276() { + Map files = generateFromContract("src/test/resources/3_0/java/issue_23276.yaml", APACHE, Map.of(), + codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); + + JavaFileAssert.assertThat(files.get("GeoJsonObject.java")) + .isNormalClass() + .doesNotExtendsClasses() + .fileContains("String type") + .fileDoesNotContain("coordinates") + .assertTypeAnnotations() + .containsWithName("JsonSubTypes"); + + JavaFileAssert.assertThat(files.get("Polygon.java")) + .extendsClass("GeoJsonObject") + .doesNotImplementInterfaces("GeoJsonObject") + .fileContains("List coordinates"); + } + + @Test + void issue_15() { + Map files = generateFromContract("src/test/resources/3_0/composed-oneof.yaml", APACHE, + Map.of(), + codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true") + .addInlineSchemaOption("REFACTOR_ALLOF_INLINE_SCHEMAS", "true")); + + JavaFileAssert.assertThat(files.get("GeoJsonObject.java")) + .isNormalClass() + .doesNotExtendsClasses() + .fileContains("String type") + .fileDoesNotContain("coordinates") + .assertTypeAnnotations() + .containsWithName("JsonSubTypes"); + + JavaFileAssert.assertThat(files.get("Polygon.java")) + .extendsClass("GeoJsonObject") + .doesNotImplementInterfaces("GeoJsonObject") + .fileContains("List coordinates"); + } + + + @Test + void issue_912() { + Map files = generateFromContract("src/test/resources/3_0/java/issue_912.yaml", APACHE, + Map.of(), + codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true") + .addInlineSchemaOption("REFACTOR_ALLOF_INLINE_SCHEMAS", "true")); + + + } + + @Test + void issue_19261() { + Map files = generateFromContract("src/test/resources/3_0/java/issue_19261.yaml", APACHE, + Map.of(), + codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true") + .addInlineSchemaOption("REFACTOR_ALLOF_INLINE_SCHEMAS", "true")); + + } + } 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 2275ab59c5ee..7791416126c3 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 @@ -62,6 +62,7 @@ import static org.openapitools.codegen.TestUtils.*; import static org.openapitools.codegen.languages.AbstractJavaCodegen.GENERATE_BUILDERS; import static org.openapitools.codegen.languages.AbstractJavaCodegen.GENERATE_CONSTRUCTOR_WITH_ALL_ARGS; +import static org.openapitools.codegen.languages.JavaClientCodegen.APACHE; import static org.openapitools.codegen.languages.JavaClientCodegen.USE_SPRING_BOOT4; import static org.openapitools.codegen.languages.SpringCodegen.*; import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.ANNOTATION_LIBRARY; @@ -6695,4 +6696,21 @@ void replaceOneOfByDiscriminatorMapping(String file) throws IOException { .fileContains("List coordinates"); } + + @Test + void issue_19261() throws IOException { + Map files = generateFromContract("src/test/resources/3_0/java/issue_19261.yaml", SPRING_BOOT, + Map.of(), + codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); + + } + + @Test + void issue_22013() throws IOException { + Map files = generateFromContract("src/test/resources/3_0/java/issue_22013.yaml", SPRING_BOOT, + Map.of(), + codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); + + assertFileContains(files.get("SomeEndponintApiController.java").toPath(),".Nullable"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml b/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml index b9afdc68c9fe..d410212560a6 100644 --- a/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml @@ -11,34 +11,34 @@ paths: responses: '200': description: OK - content: - application/json: - schema: - oneOf: - - $ref: '#/components/schemas/ObjA' - - $ref: '#/components/schemas/ObjB' - - $ref: '#/components/schemas/ObjD' - discriminator: - propertyName: realtype - mapping: - a-type: '#/components/schemas/ObjA' - b-type: '#/components/schemas/ObjB' - d-type: '#/components/schemas/ObjD' +# content: +# application/json: +# schema: +# oneOf: +# - $ref: '#/components/schemas/ObjA' +# - $ref: '#/components/schemas/ObjB' +# - $ref: '#/components/schemas/ObjD' +# discriminator: +# propertyName: realtype +# mapping: +# a-type: '#/components/schemas/ObjA' +# b-type: '#/components/schemas/ObjB' +# d-type: '#/components/schemas/ObjD' post: operationId: createState - requestBody: - content: - application/json: - schema: - oneOf: - - $ref: '#/components/schemas/ObjA' - - $ref: '#/components/schemas/ObjB' - discriminator: - propertyName: realtype - mapping: - a-type: '#/components/schemas/ObjA' - b-type: '#/components/schemas/ObjB' - required: true +# requestBody: +# content: +# application/json: +# schema: +# oneOf: +# - $ref: '#/components/schemas/ObjA' +# - $ref: '#/components/schemas/ObjB' +# discriminator: +# propertyName: realtype +# mapping: +# a-type: '#/components/schemas/ObjA' +# b-type: '#/components/schemas/ObjB' +# required: true responses: '201': description: OK diff --git a/modules/openapi-generator/src/test/resources/3_0/java/issue_19261.yaml b/modules/openapi-generator/src/test/resources/3_0/java/issue_19261.yaml new file mode 100644 index 000000000000..13ae4be13f3c --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/java/issue_19261.yaml @@ -0,0 +1,79 @@ +openapi: 3.0.3 +info: + title: test + description: Test + version: "2.0" +servers: + - url: 'https://example.com' +security: + - APIKeyHeader: [] +paths: + /products/{identifier}: + get: + tags: + - Product Item + summary: Get single product + parameters: + - name: identifier + in: path + required: true + schema: + type: string + example: "100001951" + description: The product identifier + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Product' +components: + schemas: +#v + HomeProduct: + allOf: + - $ref: "#/components/schemas/Product" + - type: object + required: + - newField + properties: + newField: + type: string + InternetProduct: + allOf: + - $ref: "#/components/schemas/Product" + - type: object + required: + - extraField + properties: + extraField: + type: string + Product: + type: object + required: + - productId + - name + - type + properties: + productId: + type: string + name: + type: string + type: + $ref: '#/components/schemas/AboType' + + + oneOf: + - $ref: '#/components/schemas/HomeProduct' + - $ref: '#/components/schemas/InternetProduct' + discriminator: + propertyName: type + mapping: + home: '#/components/schemas/HomeProduct' + internet: '#/components/schemas/InternetProduct' + AboType: + type: string + enum: + - "internet" + - "home" \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/java/issue_22013.yaml b/modules/openapi-generator/src/test/resources/3_0/java/issue_22013.yaml new file mode 100644 index 000000000000..c69b6db33938 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/java/issue_22013.yaml @@ -0,0 +1,55 @@ +openapi: 3.0.1 +info: + title: some + version: 1.0.0 +servers: + - url: http://localhost:8080 + description: Local server + + +paths: + "/someEndponint": + get: + summary: Get the specified device job of a single device + description: | + Obtain a specified device job of a single device + + operationId: someEndpoint + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/main' + +components: + schemas: + main: + oneOf: + - "$ref": "#/components/schemas/A" + - "$ref": "#/components/schemas/B" + discriminator: + propertyName: jobType + mapping: + A: "#/components/schemas/A" + B: "#/components/schemas/B" + A: +# allOf: +# - $ref: '#/components/schemas/main' + type: object + properties: + jobType: + type: string + propertyA: + type: string + + B: +# allOf: +# - $ref: '#/components/schemas/main' + type: object + properties: + jobType: + type: string + propertyB: + type: string \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/java/issue_23276.yaml b/modules/openapi-generator/src/test/resources/3_0/java/issue_23276.yaml new file mode 100644 index 000000000000..4a964e6a80a2 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/java/issue_23276.yaml @@ -0,0 +1,120 @@ +openapi: 3.0.3 +info: + version: v1 + title: Device service events +paths: {} +components: + schemas: + DeviceLifecycleEvent: + type: object + description: Notification about device lifecycle events + required: + - type + - header + - payload + properties: + type: + type: string + enum: + - device_datatransfer_status_changed + - device_communication_mode_status_changed + header: + $ref: "#/components/schemas/DeviceMessageHeader" + payload: + $ref: "#/components/schemas/Payload" + Payload: + oneOf: + - $ref: "#/components/schemas/DeviceDataTransferEventPayload" + - $ref: "#/components/schemas/DeviceCommModeStatusEventPayload" + discriminator: + propertyName: type + mapping: + device_datatransfer_status_changed: "#/components/schemas/DeviceDataTransferEventPayload" + device_communication_mode_status_changed: "#/components/schemas/DeviceCommModeStatusEventPayload" + + DeviceMessageHeader: + type: object + required: + - ts + - correlationId + - deviceId + properties: + correlationId: + type: string + deviceId: + type: string + ts: + type: string + format: date-time + + DeviceDataTransferEventPayload: + type: object + required: + - deviceId + - serialNumber + - updatedAt + - eventData + properties: + deviceId: + type: string + serialNumber: + type: string + updatedAt: + type: string + eventData: + $ref: "#/components/schemas/DeviceDataTransferEventData" + + DeviceCommModeStatusEventPayload: + type: object + required: + - deviceId + - serialNumber + - updatedAt + - eventData + properties: + deviceId: + type: string + serialNumber: + type: string + updatedAt: + type: string + eventData: + $ref: "#/components/schemas/DeviceCommModeStatusEventData" + + DeviceDataTransferEventData: + type: object + required: + - StatusDetails + properties: + StatusDetails: + type: object + properties: + dataTransfer: + type: object + properties: + status: + type: string + enum: [capable, incapable, Not Applicable] + statusReason: + type: string + updatedAt: + type: string + isMuted: + type: boolean + + DeviceCommModeStatusEventData: + type: object + required: + - StatusDetails + properties: + StatusDetails: + type: object + properties: + communicationMode: + type: object + properties: + runtimeCommMode: + type: string + enum: [limited, normal] + updatedAt: + type: string \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/java/issue_912.yaml b/modules/openapi-generator/src/test/resources/3_0/java/issue_912.yaml new file mode 100644 index 000000000000..fdbac0616860 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/java/issue_912.yaml @@ -0,0 +1,79 @@ +--- +openapi: 3.0.1 +info: + title: Mukul + version: '2.0' +paths: + "/catalog/{id}": + get: + operationId: getCatalogItem + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + default: + description: default response + content: + application/json: + schema: + "$ref": "#/components/schemas/CatalogEntity" +components: + schemas: + CatalogEntity: + type: object + properties: + id: + type: string + entityType: + type: string + discriminator: + propertyName: entityType + mapping: + folder: "#/components/schemas/Folder" + source: "#/components/schemas/Source" + oneOf: + - "$ref": "#/components/schemas/Folder" + - "$ref": "#/components/schemas/Source" + Folder: + type: object + properties: + path: + type: array + items: + type: string + tag: + type: string + children: + type: array + items: + "$ref": "#/components/schemas/CatalogItem" + allOf: + - "$ref": "#/components/schemas/CatalogEntity" + CatalogItem: + type: object + properties: + name: + type: string + Source: + type: object + properties: + tag: + type: string + type: + type: string + name: + type: string + description: + type: string + createdAt: + type: string + format: date-time + children: + type: array + items: + "$ref": "#/components/schemas/CatalogItem" + allOf: + - "$ref": "#/components/schemas/CatalogEntity" diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml index 349d3ce75a7d..54058dda434b 100644 --- a/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.3 +\openapi: 3.0.3 info: title: GeoJSON Discriminator Test version: 1.0.0 From bbc0fd051de56b6e7102a9dc910eed60c25470a2 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Fri, 17 Apr 2026 17:27:04 +0200 Subject: [PATCH 03/22] Improvements --- .../codegen/OpenAPINormalizer.java | 32 +++- .../codegen/OpenAPINormalizerTest.java | 2 +- .../org/openapitools/codegen/TestUtils.java | 18 +-- .../codegen/java/JavaClientCodegenTest.java | 68 +++----- .../assertions/AbstractAnnotationsAssert.java | 31 ++++ .../java/spring/SpringCodegenTest.java | 145 ++++++++++++------ .../test/resources/3_0/composed-oneof.yaml | 50 +++--- .../test/resources/3_0/java/issue_23276.yaml | 120 --------------- ...ssue_19261.yaml => oneOf_issue_19261.yaml} | 2 - ...ssue_22013.yaml => oneOf_issue_22013.yaml} | 6 - ...ssue_23527.yaml => oneOf_issue_23527.yaml} | 7 +- ..._23527_1.yaml => oneOf_issue_23527_1.yaml} | 0 ..._23527_2.yaml => oneOf_issue_23527_2.yaml} | 0 .../test/resources/3_0/oneof_issue_23577.yaml | 51 ++++++ 14 files changed, 261 insertions(+), 271 deletions(-) delete mode 100644 modules/openapi-generator/src/test/resources/3_0/java/issue_23276.yaml rename modules/openapi-generator/src/test/resources/3_0/{java/issue_19261.yaml => oneOf_issue_19261.yaml} (99%) rename modules/openapi-generator/src/test/resources/3_0/{java/issue_22013.yaml => oneOf_issue_22013.yaml} (89%) rename modules/openapi-generator/src/test/resources/3_0/{spring/issue_23527.yaml => oneOf_issue_23527.yaml} (87%) rename modules/openapi-generator/src/test/resources/3_0/{spring/issue_23527_1.yaml => oneOf_issue_23527_1.yaml} (100%) rename modules/openapi-generator/src/test/resources/3_0/{spring/issue_23527_2.yaml => oneOf_issue_23527_2.yaml} (100%) create mode 100644 modules/openapi-generator/src/test/resources/3_0/oneof_issue_23577.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index b1003d6314a8..067d69198f6e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1603,7 +1603,7 @@ protected Schema processReplaceOneOfByMapping(Schema schema) { Schema oneOf = (Schema) oneOfObject; String refSchema = oneOf.get$ref(); if (refSchema != null) { - String name = refSchema.contains("/") ? refSchema.substring(refSchema.lastIndexOf('/') + 1) : refSchema; + String name = getDiscriminatorValue(refSchema); mappings.put(name, refSchema); } } @@ -1615,12 +1615,26 @@ protected Schema processReplaceOneOfByMapping(Schema schema) { return schema; } + protected String getDiscriminatorValue(String refSchema) { + String schemaName = refSchema.contains("/") ? refSchema.substring(refSchema.lastIndexOf('/') + 1) : refSchema;; + Schema schema = ModelUtils.getSchema(openAPI, schemaName); + if (schema != null && schema.getExtensions() != null) { + String discriminatorValue = String.valueOf(schema.getExtensions().get("x-discriminator-value")); + if (discriminatorValue != null) { + return discriminatorValue; + } + } + return schemaName; + } + protected void ensureInheritance(Schema parent, String parentName) { Discriminator discriminator = parent.getDiscriminator(); if (discriminator != null && discriminator.getMapping() != null) { - for (String mapping : discriminator.getMapping().keySet()) { - Schema child = ModelUtils.getSchema(openAPI, mapping); + + for (String mapping : discriminator.getMapping().values()) { + String refSchemaName = getDiscriminatorValue(mapping); + Schema child = ModelUtils.getSchema(openAPI, refSchemaName); if (child != null) { ensureInheritance(parent, child, parentName); } @@ -1629,23 +1643,27 @@ protected void ensureInheritance(Schema parent, String parentName) { } protected void ensureInheritance(Schema parent, Schema child, String parentName) { + String reference = "#/components/schemas/" + parentName; List allOf = child.getAllOf(); if (allOf != null) { - if (hasParent(child, parent)) { + if (hasParent(child, parent, reference)) { return; } } else { allOf = new ArrayList<>(); child.setAllOf(allOf); } - Schema refToParent = new Schema<>().$ref("#/components/schemas/" + parentName); + Schema refToParent = new Schema<>().$ref(reference); allOf.add(refToParent); } - private boolean hasParent(Schema child, Schema parent) { + private boolean hasParent(Schema child, Schema parent, String reference) { + if (child.get$ref() != null && child.get$ref().equals(reference)) { + return true; + } List allOf = child.getAllOf(); if (allOf != null) { - return allOf.stream().anyMatch(s -> hasParent(s, parent)); + return allOf.stream().anyMatch(s -> hasParent(s, parent, reference)); } return false; } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index e72447f3f20b..020781c875c6 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -1508,7 +1508,7 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { @Test public void testREPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING() { // to test array schema processing in 3.1 spec - OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/spring/issue_23527.yaml"); + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/spring/oneOf_issue_23527.yaml"); Map inputRules = Map.of("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, inputRules); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/TestUtils.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/TestUtils.java index fbf55bd66de0..a6df487ef840 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/TestUtils.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/TestUtils.java @@ -15,7 +15,6 @@ import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.servers.Server; import io.swagger.v3.parser.core.models.ParseOptions; -import org.apache.commons.io.FileUtils; import org.openapitools.codegen.java.assertions.JavaFileAssert; import org.openapitools.codegen.model.ModelMap; import org.openapitools.codegen.model.ModelsMap; @@ -343,23 +342,14 @@ public static ModelsMap createCodegenModelWrapper(CodegenModel cm) { } public static Path newTempFolder() { - File file = new File("c:\\temp\\generated"); - file.mkdir(); + final Path tempDir; try { - FileUtils.cleanDirectory(file); + tempDir = Files.createTempDirectory("test"); } catch (IOException e) { throw new RuntimeException(e); } + tempDir.toFile().deleteOnExit(); - return file.toPath(); -// final Path tempDir; -// try { -// tempDir = Files.createTempDirectory("test"); -// } catch (IOException e) { -// throw new RuntimeException(e); -// } -// tempDir.toFile().deleteOnExit(); -// -// return tempDir; + return tempDir; } } 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 6a825721357e..2142dec9e289 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 @@ -4330,9 +4330,9 @@ public void testJspecify(String library, boolean useSpringBoot4, boolean hasJspe @DataProvider(name = "replaceOneOf") public Object[][] replaceOneOf() { return new Object[][]{ - {"src/test/resources/3_0/spring/issue_23527.yaml"}, - {"src/test/resources/3_0/spring/issue_23527_1.yaml"}, - {"src/test/resources/3_0/spring/issue_23527_2.yaml"} + {"src/test/resources/3_0/oneOf_issue_23527.yaml"}, + {"src/test/resources/3_0/oneOf_issue_23527_1.yaml"}, + {"src/test/resources/3_0/oneOf_issue_23527_2.yaml"} }; } @@ -4355,64 +4355,42 @@ void replaceOneOfByDiscriminatorMapping(String file) { .fileContains("List coordinates"); } - @Test - void issue_23276() { - Map files = generateFromContract("src/test/resources/3_0/java/issue_23276.yaml", APACHE, Map.of(), - codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); - - JavaFileAssert.assertThat(files.get("GeoJsonObject.java")) - .isNormalClass() - .doesNotExtendsClasses() - .fileContains("String type") - .fileDoesNotContain("coordinates") - .assertTypeAnnotations() - .containsWithName("JsonSubTypes"); - - JavaFileAssert.assertThat(files.get("Polygon.java")) - .extendsClass("GeoJsonObject") - .doesNotImplementInterfaces("GeoJsonObject") - .fileContains("List coordinates"); - } @Test - void issue_15() { - Map files = generateFromContract("src/test/resources/3_0/composed-oneof.yaml", APACHE, + void issue_912() { + Map files = generateFromContract("src/test/resources/3_0/java/issue_912.yaml", APACHE, Map.of(), codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true") .addInlineSchemaOption("REFACTOR_ALLOF_INLINE_SCHEMAS", "true")); - - JavaFileAssert.assertThat(files.get("GeoJsonObject.java")) + JavaFileAssert.assertThat(files.get("CatalogEntity.java")) .isNormalClass() .doesNotExtendsClasses() - .fileContains("String type") - .fileDoesNotContain("coordinates") + .fileContains("String entityType") .assertTypeAnnotations() - .containsWithName("JsonSubTypes"); - - JavaFileAssert.assertThat(files.get("Polygon.java")) - .extendsClass("GeoJsonObject") - .doesNotImplementInterfaces("GeoJsonObject") - .fileContains("List coordinates"); - } - - - @Test - void issue_912() { - Map files = generateFromContract("src/test/resources/3_0/java/issue_912.yaml", APACHE, - Map.of(), - codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true") - .addInlineSchemaOption("REFACTOR_ALLOF_INLINE_SCHEMAS", "true")); - + .containsWithNameAndAttributes("JsonTypeInfo", Map.of("include", "JsonTypeInfo.As.PROPERTY", "property", "\"entityType\"")) + .containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Folder.class", "name", "\"folder\"")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Source.class", "name", "\"source\"")); } @Test void issue_19261() { - Map files = generateFromContract("src/test/resources/3_0/java/issue_19261.yaml", APACHE, + Map files = generateFromContract("src/test/resources/3_0/oneOf_issue_19261.yaml", APACHE, Map.of(), codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true") .addInlineSchemaOption("REFACTOR_ALLOF_INLINE_SCHEMAS", "true")); - + JavaFileAssert.assertThat(files.get("Product.java")) + .isNormalClass() + .doesNotExtendsClasses() + .fileContains("AboType type") + .assertTypeAnnotations() + .containsWithNameAndAttributes("JsonTypeInfo", Map.of("include", "JsonTypeInfo.As.PROPERTY", "property", "\"type\"")) + .containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "HomeProduct.class", "name", "\"home\"")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "InternetProduct.class", "name", "\"internet\"")); + JavaFileAssert.assertThat(files.get("InternetProduct.java")) + .extendsClass("Product"); } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java index 2275945f64c9..22c1027aef25 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/AbstractAnnotationsAssert.java @@ -99,6 +99,14 @@ public ACTUAL recursivelyContainsWithName(String name) { return myself(); } + public ACTUAL recursivelyContainsWithNameAndAttributes(String name, Map attributes) { + super + .withFailMessage("Should have annotation with name: " + name) + .anyMatch(annotation -> containsSpecificAnnotationNameAndAttributes(annotation, name, attributes)); + + return myself(); + } + private boolean containsSpecificAnnotationName(Node node, String name) { if (node == null || name == null) return false; @@ -118,4 +126,27 @@ private boolean containsSpecificAnnotationName(Node node, String name) { return false; } + + private boolean containsSpecificAnnotationNameAndAttributes(Node node, String name, Map attributes) { + if (node == null || name == null) + return false; + + if (node instanceof AnnotationExpr) { + AnnotationExpr annotation = (AnnotationExpr) node; + + if (annotation.getNameAsString().equals(name)) + + if (hasAttributes(annotation, attributes)) { + return true; + } + + } + + for(Node child: node.getChildNodes()) { + if (containsSpecificAnnotationNameAndAttributes(child, name, attributes)) + return true; + } + + return false; + } } 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 7791416126c3..7ad44780ef18 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 @@ -59,10 +59,10 @@ import static java.util.stream.Collectors.groupingBy; import static org.assertj.core.api.Assertions.assertThat; +import static org.openapitools.codegen.CodegenConstants.*; import static org.openapitools.codegen.TestUtils.*; import static org.openapitools.codegen.languages.AbstractJavaCodegen.GENERATE_BUILDERS; import static org.openapitools.codegen.languages.AbstractJavaCodegen.GENERATE_CONSTRUCTOR_WITH_ALL_ARGS; -import static org.openapitools.codegen.languages.JavaClientCodegen.APACHE; import static org.openapitools.codegen.languages.JavaClientCodegen.USE_SPRING_BOOT4; import static org.openapitools.codegen.languages.SpringCodegen.*; import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.ANNOTATION_LIBRARY; @@ -1979,45 +1979,33 @@ public void testOneOf5381() throws IOException { @Test public void testOneOfAndAllOf() 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/oneof_polymorphism_and_inheritance.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); // skip metadata and ↓ only generate models - generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); - generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); - generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); - generator.setGeneratorPropertyDefault(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "false"); - - codegen.setUseOneOfInterfaces(true); - codegen.setUseDeductionForOneOfInterfaces(true); - codegen.setLegacyDiscriminatorBehavior(false); - - generator.opts(input).generate(); - + Map files = generateFromContract("src/test/resources/3_0/oneof_polymorphism_and_inheritance.yaml", SPRING_BOOT, + Map.of(HATEOAS, true, GENERATE_MODEL_TESTS, false, GENERATE_MODEL_DOCS, false, LEGACY_DISCRIMINATOR_BEHAVIOR, false, + AbstractJavaCodegen.USE_ONE_OF_INTERFACES, true, + USE_DEDUCTION_FOR_ONE_OF_INTERFACES, true, + CXFServerFeatures.LOAD_TEST_DATA_FROM_FILE, "true") + ); + JavaFileAssert.assertThat(files.get("Fruit.java")) + .isInterface() + .assertTypeAnnotations().containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Apple.class", "name", "\"APPLE\"")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Banana.class", "name", "\"BANANA\"")); // test deduction - assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/Animal.java"), "@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)", "@JsonSubTypes.Type(value = Dog.class),", "@JsonSubTypes.Type(value = Cat.class)"); - assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/Foo.java"), "public class Foo extends Entity implements FooRefOrValue"); - assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/FooRef.java"), "public class FooRef extends EntityRef implements FooRefOrValue"); - assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/FooRefOrValue.java"), "public interface FooRefOrValue"); + JavaFileAssert.assertThat(files.get("Animal.java")) + .isInterface() + .assertTypeAnnotations().containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Dog.class")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Cat.class")) + .containsWithNameAndAttributes("JsonTypeInfo", Map.of("use", "JsonTypeInfo.Id.DEDUCTION")); + + assertFileContains(files.get("Foo.java").toPath(), "public class Foo extends Entity implements FooRefOrValue"); + assertFileContains(files.get("FooRef.java").toPath(), "public class FooRef extends EntityRef implements FooRefOrValue"); + assertFileContains(files.get("FooRefOrValue.java").toPath(), "public interface FooRefOrValue"); // previous bugs - JavaFileAssert.assertThat(Paths.get(outputPath + "/src/main/java/org/openapitools/model/BarRef.java")) + JavaFileAssert.assertThat(files.get("BarRef.java")) .fileDoesNotContain("atTypesuper.hashCode", "private String atBaseType"); // imports for inherited properties - assertFileContains(Paths.get(outputPath + "/src/main/java/org/openapitools/model/PizzaSpeziale.java"), "import java.math.BigDecimal"); + assertFileContains(files.get("PizzaSpeziale.java").toPath(), "import java.math.BigDecimal"); } @Test @@ -6670,15 +6658,16 @@ public void testJspecify(String library, int springBootVersion, String fooApiFil @DataProvider(name = "replaceOneOf") public Object[][] replaceOneOf() { return new Object[][]{ - {"src/test/resources/3_0/spring/issue_23527.yaml"}, - {"src/test/resources/3_0/spring/issue_23527_1.yaml"}, - {"src/test/resources/3_0/spring/issue_23527_2.yaml"} + {"src/test/resources/3_0/oneOf_issue_23527.yaml"}, + {"src/test/resources/3_0/oneOf_issue_23527_1.yaml"}, + {"src/test/resources/3_0/oneOf_issue_23527_2.yaml"} }; } @Test(dataProvider = "replaceOneOf" ) void replaceOneOfByDiscriminatorMapping(String file) throws IOException { - Map files = generateFromContract(file, SPRING_BOOT, Map.of(), + Map files = generateFromContract(file, SPRING_BOOT, + Map.of(GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true), codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); JavaFileAssert.assertThat(files.get("GeoJsonObject.java")) @@ -6687,30 +6676,90 @@ void replaceOneOfByDiscriminatorMapping(String file) throws IOException { .fileContains("String type") .fileDoesNotContain("coordinates") .assertTypeAnnotations() + .containsWithNameAndAttributes("JsonTypeInfo", Map.of("include", "JsonTypeInfo.As.PROPERTY", "property", "\"type\"")) .containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "MultiPolygon.class", "name", "\"MultiPolygon\"")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Polygon.class", "name", "\"Polygon\"")); ; JavaFileAssert.assertThat(files.get("Polygon.java")) .extendsClass("GeoJsonObject") .doesNotImplementInterfaces("GeoJsonObject") - .fileContains("List coordinates"); - + .fileContains("List coordinates") + .fileDoesNotContain("@JsonSubTypes"); } @Test void issue_19261() throws IOException { - Map files = generateFromContract("src/test/resources/3_0/java/issue_19261.yaml", SPRING_BOOT, - Map.of(), + Map files = generateFromContract("src/test/resources/3_0/oneOf_issue_19261.yaml", SPRING_BOOT, + Map.of(GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true), codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); - + JavaFileAssert.assertThat(files.get("Product.java")) + .isNormalClass() + .doesNotExtendsClasses() + .fileContains("AboType type") + .assertTypeAnnotations() + .containsWithNameAndAttributes("JsonTypeInfo", Map.of("include", "JsonTypeInfo.As.PROPERTY", "property", "\"type\"")) + .containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "HomeProduct.class", "name", "\"home\"")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "InternetProduct.class", "name", "\"internet\"")); + JavaFileAssert.assertThat(files.get("InternetProduct.java")) + .extendsClass("Product"); } @Test void issue_22013() throws IOException { - Map files = generateFromContract("src/test/resources/3_0/java/issue_22013.yaml", SPRING_BOOT, - Map.of(), + Map files = generateFromContract("src/test/resources/3_0/oneOf_issue_22013.yaml", SPRING_BOOT, + Map.of(GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true), + codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); + JavaFileAssert.assertThat(files.get("Main.java")) + .isNormalClass() + .doesNotExtendsClasses() + .fileDoesNotContain("String jobType") + .assertTypeAnnotations() + .containsWithNameAndAttributes("JsonTypeInfo", Map.of("include", "JsonTypeInfo.As.PROPERTY", "property", "\"jobType\"")) + .containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "A.class", "name", "\"A\"")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "B.class", "name", "\"B\"")); + JavaFileAssert.assertThat(files.get("B.java")) + .extendsClass("Main") + .fileContains("String jobType;"); + } + + @Test + void issue_23577() throws IOException { + Map files = generateFromContract("src/test/resources/3_0/oneOf_issue_23577.yaml", SPRING_BOOT, + Map.of(GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true), + codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); + JavaFileAssert.assertThat(files.get("Event.java")) + .isNormalClass() + .doesNotExtendsClasses() + .fileDoesNotContain("String type") + .assertTypeAnnotations() + .containsWithNameAndAttributes("JsonTypeInfo", Map.of("include", "JsonTypeInfo.As.PROPERTY", "property", "\"type\"")) + .containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "CreatedEvent.class", "name", "\"created\"")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "UpdatedEvent.class", "name", "\"updated\"")); + JavaFileAssert.assertThat(files.get("CreatedEvent.java")) + .extendsClass("Event") + .implementsInterfaces("com.example.Notification") + .fileContains("String type;"); + } + + @Test + void oneof_polymorphism_and_inheritance() throws IOException { + Map files = generateFromContract("src/test/resources/3_0/oneof_polymorphism_and_inheritance.yaml", SPRING_BOOT, + Map.of(MODEL_NAME_SUFFIX, "Dto", + GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true), codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); + JavaFileAssert.assertThat(files.get("FruitDto.java")) + .isNormalClass() + .assertTypeAnnotations().containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "AppleDto.class", "name", "\"APPLE\"")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "BananaDto.class", "name", "\"BANANA\"")); - assertFileContains(files.get("SomeEndponintApiController.java").toPath(),".Nullable"); + JavaFileAssert.assertThat(files.get("BananaDto.java")) + .isNormalClass() + .extendsClass("FruitDto"); } } diff --git a/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml b/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml index d410212560a6..87526303be38 100644 --- a/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml @@ -11,33 +11,33 @@ paths: responses: '200': description: OK -# content: -# application/json: -# schema: -# oneOf: -# - $ref: '#/components/schemas/ObjA' -# - $ref: '#/components/schemas/ObjB' -# - $ref: '#/components/schemas/ObjD' -# discriminator: -# propertyName: realtype -# mapping: -# a-type: '#/components/schemas/ObjA' -# b-type: '#/components/schemas/ObjB' -# d-type: '#/components/schemas/ObjD' + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ObjA' + - $ref: '#/components/schemas/ObjB' + - $ref: '#/components/schemas/ObjD' + discriminator: + propertyName: realtype + mapping: + a-type: '#/components/schemas/ObjA' + b-type: '#/components/schemas/ObjB' + d-type: '#/components/schemas/ObjD' post: operationId: createState -# requestBody: -# content: -# application/json: -# schema: -# oneOf: -# - $ref: '#/components/schemas/ObjA' -# - $ref: '#/components/schemas/ObjB' -# discriminator: -# propertyName: realtype -# mapping: -# a-type: '#/components/schemas/ObjA' -# b-type: '#/components/schemas/ObjB' + requestBody: + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/ObjA' + - $ref: '#/components/schemas/ObjB' + discriminator: + propertyName: realtype + mapping: + a-type: '#/components/schemas/ObjA' + b-type: '#/components/schemas/ObjB' # required: true responses: '201': diff --git a/modules/openapi-generator/src/test/resources/3_0/java/issue_23276.yaml b/modules/openapi-generator/src/test/resources/3_0/java/issue_23276.yaml deleted file mode 100644 index 4a964e6a80a2..000000000000 --- a/modules/openapi-generator/src/test/resources/3_0/java/issue_23276.yaml +++ /dev/null @@ -1,120 +0,0 @@ -openapi: 3.0.3 -info: - version: v1 - title: Device service events -paths: {} -components: - schemas: - DeviceLifecycleEvent: - type: object - description: Notification about device lifecycle events - required: - - type - - header - - payload - properties: - type: - type: string - enum: - - device_datatransfer_status_changed - - device_communication_mode_status_changed - header: - $ref: "#/components/schemas/DeviceMessageHeader" - payload: - $ref: "#/components/schemas/Payload" - Payload: - oneOf: - - $ref: "#/components/schemas/DeviceDataTransferEventPayload" - - $ref: "#/components/schemas/DeviceCommModeStatusEventPayload" - discriminator: - propertyName: type - mapping: - device_datatransfer_status_changed: "#/components/schemas/DeviceDataTransferEventPayload" - device_communication_mode_status_changed: "#/components/schemas/DeviceCommModeStatusEventPayload" - - DeviceMessageHeader: - type: object - required: - - ts - - correlationId - - deviceId - properties: - correlationId: - type: string - deviceId: - type: string - ts: - type: string - format: date-time - - DeviceDataTransferEventPayload: - type: object - required: - - deviceId - - serialNumber - - updatedAt - - eventData - properties: - deviceId: - type: string - serialNumber: - type: string - updatedAt: - type: string - eventData: - $ref: "#/components/schemas/DeviceDataTransferEventData" - - DeviceCommModeStatusEventPayload: - type: object - required: - - deviceId - - serialNumber - - updatedAt - - eventData - properties: - deviceId: - type: string - serialNumber: - type: string - updatedAt: - type: string - eventData: - $ref: "#/components/schemas/DeviceCommModeStatusEventData" - - DeviceDataTransferEventData: - type: object - required: - - StatusDetails - properties: - StatusDetails: - type: object - properties: - dataTransfer: - type: object - properties: - status: - type: string - enum: [capable, incapable, Not Applicable] - statusReason: - type: string - updatedAt: - type: string - isMuted: - type: boolean - - DeviceCommModeStatusEventData: - type: object - required: - - StatusDetails - properties: - StatusDetails: - type: object - properties: - communicationMode: - type: object - properties: - runtimeCommMode: - type: string - enum: [limited, normal] - updatedAt: - type: string \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/java/issue_19261.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_19261.yaml similarity index 99% rename from modules/openapi-generator/src/test/resources/3_0/java/issue_19261.yaml rename to modules/openapi-generator/src/test/resources/3_0/oneOf_issue_19261.yaml index 13ae4be13f3c..44a8223a8042 100644 --- a/modules/openapi-generator/src/test/resources/3_0/java/issue_19261.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_19261.yaml @@ -62,8 +62,6 @@ components: type: string type: $ref: '#/components/schemas/AboType' - - oneOf: - $ref: '#/components/schemas/HomeProduct' - $ref: '#/components/schemas/InternetProduct' diff --git a/modules/openapi-generator/src/test/resources/3_0/java/issue_22013.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_22013.yaml similarity index 89% rename from modules/openapi-generator/src/test/resources/3_0/java/issue_22013.yaml rename to modules/openapi-generator/src/test/resources/3_0/oneOf_issue_22013.yaml index c69b6db33938..5901ceb6a282 100644 --- a/modules/openapi-generator/src/test/resources/3_0/java/issue_22013.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_22013.yaml @@ -5,8 +5,6 @@ info: servers: - url: http://localhost:8080 description: Local server - - paths: "/someEndponint": get: @@ -35,8 +33,6 @@ components: A: "#/components/schemas/A" B: "#/components/schemas/B" A: -# allOf: -# - $ref: '#/components/schemas/main' type: object properties: jobType: @@ -45,8 +41,6 @@ components: type: string B: -# allOf: -# - $ref: '#/components/schemas/main' type: object properties: jobType: diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23527.yaml similarity index 87% rename from modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml rename to modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23527.yaml index 54058dda434b..8c923f70cf08 100644 --- a/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23527.yaml @@ -1,4 +1,4 @@ -\openapi: 3.0.3 +openapi: 3.0.3 info: title: GeoJSON Discriminator Test version: 1.0.0 @@ -16,7 +16,7 @@ components: propertyName: type oneOf: - $ref: '#/components/schemas/Polygon' - - $ref: '#/components/schemas/MultiPolygon' + - $ref: '#/components/schemas/Multi-Polygon' Polygon: allOf: - $ref: '#/components/schemas/GeoJsonObject' @@ -27,7 +27,8 @@ components: items: type: number format: double - MultiPolygon: + Multi-Polygon: + x-discriminator-value: MultiPolygon allOf: - $ref: '#/components/schemas/GeoJsonObject' - type: object diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_1.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23527_1.yaml similarity index 100% rename from modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_1.yaml rename to modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23527_1.yaml diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_2.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23527_2.yaml similarity index 100% rename from modules/openapi-generator/src/test/resources/3_0/spring/issue_23527_2.yaml rename to modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23527_2.yaml diff --git a/modules/openapi-generator/src/test/resources/3_0/oneof_issue_23577.yaml b/modules/openapi-generator/src/test/resources/3_0/oneof_issue_23577.yaml new file mode 100644 index 000000000000..544991ba6c64 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneof_issue_23577.yaml @@ -0,0 +1,51 @@ +openapi: 3.0.3 +info: + title: Issue reproducer + description: Issue reproducer + version: 0.1.0 +paths: + /reproducer: + get: + operationId: 'reproduce' + responses: + '200': + description: result + content: + application/json: + schema: + $ref: '#/components/schemas/Event' + +components: + schemas: + Event: + oneOf: + - $ref: '#/components/schemas/CreatedEvent' + - $ref: '#/components/schemas/UpdatedEvent' + discriminator: + propertyName: type + mapping: + created: '#/components/schemas/CreatedEvent' + updated: '#/components/schemas/UpdatedEvent' + + CreatedEvent: + x-implements: com.example.Notification + type: object + properties: + type: + type: string + value: + type: string + required: + - type + - value + + UpdatedEvent: + type: object + properties: + type: + type: string + value: + type: string + required: + - type + - value \ No newline at end of file From af8f6c254f3f23693a4b8d10366a9a20839b3165 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Fri, 17 Apr 2026 18:08:10 +0200 Subject: [PATCH 04/22] Merge master --- .../org/openapitools/codegen/languages/SpringCodegen.java | 6 ------ 1 file changed, 6 deletions(-) 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 a44a8bf1f302..e6a150c4e21a 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 @@ -845,15 +845,10 @@ public void preprocessOpenAPI(OpenAPI openAPI) { @Override public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { - if (isSpringCodegen()) { - Map newImport = Map.of("import", importMapping.get("Nullable")); - objs.getImports().add(newImport); - } final OperationMap operations = objs.getOperations(); if (operations != null) { final List ops = operations.getOperation(); for (final CodegenOperation operation : ops) { - final List responses = operation.responses; if (responses != null) { for (final CodegenResponse resp : responses) { @@ -899,7 +894,6 @@ public void setIsVoid(boolean isVoid) { prepareVersioningParameters(ops); handleImplicitHeaders(operation); - } // The tag for the controller is the first tag of the first operation final CodegenOperation firstOperation = ops.get(0); From c963666187677a7252b1aa9babd628adb77dcbcd Mon Sep 17 00:00:00 2001 From: jpfinne Date: Fri, 17 Apr 2026 18:19:48 +0200 Subject: [PATCH 05/22] Fix invalid path --- .../codegen/OpenAPINormalizerTest.java | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 020781c875c6..34422aa8d139 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -1508,26 +1508,13 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { @Test public void testREPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING() { // to test array schema processing in 3.1 spec - OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/spring/oneOf_issue_23527.yaml"); + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/oneOf_issue_23527.yaml"); Map inputRules = Map.of("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, inputRules); openAPINormalizer.normalize(); - dump(openAPI); - } - - private void dump(OpenAPI openAPI) { - - ObjectMapper mapper = Yaml.mapper(); - String yaml = null; - try { - yaml = mapper.writeValueAsString(openAPI); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - - System.out.println(yaml); } + } From 848e606e809e8314eeaf9a93daa87dd823c2f069 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Fri, 17 Apr 2026 18:23:30 +0200 Subject: [PATCH 06/22] Improve assertions --- .../java/org/openapitools/codegen/OpenAPINormalizerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 34422aa8d139..265b743d79bc 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -1514,6 +1514,9 @@ public void testREPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING() { OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, inputRules); openAPINormalizer.normalize(); + Schema geoJsonObject = openAPI.getComponents().getSchemas().get("GeoJsonObject"); + Map mapping = geoJsonObject.getDiscriminator().getMapping(); + assertEquals(mapping, Map.of("MultiPolygon", "#/components/schemas/Multi-Polygon", "Polygon", "#/components/schemas/Polygon" )); } From 73e1f6e00216dcd74a406b62808d76603d8a89ff Mon Sep 17 00:00:00 2001 From: jpfinne Date: Fri, 17 Apr 2026 18:38:47 +0200 Subject: [PATCH 07/22] Fix invalid discriminator value --- .../main/java/org/openapitools/codegen/OpenAPINormalizer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 067d69198f6e..d2acddad3d2e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1619,7 +1619,7 @@ protected String getDiscriminatorValue(String refSchema) { String schemaName = refSchema.contains("/") ? refSchema.substring(refSchema.lastIndexOf('/') + 1) : refSchema;; Schema schema = ModelUtils.getSchema(openAPI, schemaName); if (schema != null && schema.getExtensions() != null) { - String discriminatorValue = String.valueOf(schema.getExtensions().get("x-discriminator-value")); + String discriminatorValue = Objects.toString(schema.getExtensions().get("x-discriminator-value"), null); if (discriminatorValue != null) { return discriminatorValue; } From 716688ea349931a3f6bf8760615634c03344a708 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Fri, 17 Apr 2026 19:00:36 +0200 Subject: [PATCH 08/22] filename case --- .../main/java/org/openapitools/codegen/OpenAPINormalizer.java | 4 ++-- .../3_0/{oneof_issue_23577.yaml => oneOf_issue_23577_.yaml} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename modules/openapi-generator/src/test/resources/3_0/{oneof_issue_23577.yaml => oneOf_issue_23577_.yaml} (100%) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index d2acddad3d2e..7058ff9402cd 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1619,9 +1619,9 @@ protected String getDiscriminatorValue(String refSchema) { String schemaName = refSchema.contains("/") ? refSchema.substring(refSchema.lastIndexOf('/') + 1) : refSchema;; Schema schema = ModelUtils.getSchema(openAPI, schemaName); if (schema != null && schema.getExtensions() != null) { - String discriminatorValue = Objects.toString(schema.getExtensions().get("x-discriminator-value"), null); + Object discriminatorValue = schema.getExtensions().get("x-discriminator-value"); if (discriminatorValue != null) { - return discriminatorValue; + return discriminatorValue.toString(); } } return schemaName; diff --git a/modules/openapi-generator/src/test/resources/3_0/oneof_issue_23577.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23577_.yaml similarity index 100% rename from modules/openapi-generator/src/test/resources/3_0/oneof_issue_23577.yaml rename to modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23577_.yaml From cef6534f68d2d29eec4c099e971dec8d3411bcd8 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Fri, 17 Apr 2026 19:00:56 +0200 Subject: [PATCH 09/22] filename case --- .../3_0/{oneOf_issue_23577_.yaml => oneOf_issue_23577.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/openapi-generator/src/test/resources/3_0/{oneOf_issue_23577_.yaml => oneOf_issue_23577.yaml} (100%) diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23577_.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23577.yaml similarity index 100% rename from modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23577_.yaml rename to modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23577.yaml From bcf2047890bb01b966a22081ad99ed062dc12059 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Fri, 17 Apr 2026 19:24:53 +0200 Subject: [PATCH 10/22] Rollback composed-oneof.yaml --- .../src/test/resources/3_0/composed-oneof.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml b/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml index 87526303be38..b9afdc68c9fe 100644 --- a/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/composed-oneof.yaml @@ -38,7 +38,7 @@ paths: mapping: a-type: '#/components/schemas/ObjA' b-type: '#/components/schemas/ObjB' -# required: true + required: true responses: '201': description: OK From 8319043fdc3aca8c05a44b6a0a558219ec01bbce Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 18 Apr 2026 16:04:59 +0200 Subject: [PATCH 11/22] Improve normalization --- .../codegen/OpenAPINormalizer.java | 162 +++++++++++++++--- .../codegen/utils/ModelUtils.java | 18 ++ .../codegen/OpenAPINormalizerTest.java | 26 ++- .../codegen/java/JavaClientCodegenTest.java | 21 --- .../java/spring/SpringCodegenTest.java | 8 + .../test/resources/3_0/oneOf_issue_14769.yaml | 45 +++++ .../test/resources/3_0/oneOf_issue_23276.yaml | 120 +++++++++++++ .../resources/3_0/oneOf_issue_23527_1.yaml | 36 ++-- 8 files changed, 371 insertions(+), 65 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/oneOf_issue_14769.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23276.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 7058ff9402cd..fbe5b75b0ccf 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -646,7 +646,7 @@ protected void normalizeComponentsSchemas() { schemas.put(schemaName, normalizeSchema(schema, new HashSet<>())); if (getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) { - ensureInheritance(schema, schemaName); + ensureInheritanceForDiscriminatorMappings(schema, schemaName); } } } @@ -1585,10 +1585,10 @@ protected Schema processSimplifyOneOf(Schema schema) { * Ensure inheritance is correctly defined for OneOf and Discriminators. * * For schemas containing oneOf and discriminator.propertyName: - * Create the mappings as $refs - * Remove OneOf - * - * For referenced schemas, ensure that there is an allOf with this schema. + *
    + *
  • Create the mappings as $refs
  • + *
  • Remove OneOf
  • + *
*/ protected Schema processReplaceOneOfByMapping(Schema schema) { if (!getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) { @@ -1596,74 +1596,190 @@ protected Schema processReplaceOneOfByMapping(Schema schema) { } Discriminator discriminator = schema.getDiscriminator(); if (discriminator != null) { - if (discriminator.getMapping() == null) { + boolean inlineSchema = isInlineSchema(schema); + if (inlineSchema) { + // the For referenced schemas, ensure that there is an allOf with this schema. + LOGGER.warn("Inline oneOf schema not supported by REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING normalization"); + return schema; + } + if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) { Map mappings = new TreeMap<>(); discriminator.setMapping(mappings); - for (Object oneOfObject : schema.getOneOf()) { - Schema oneOf = (Schema) oneOfObject; + List oneOfs = schema.getOneOf(); + for (Schema oneOf: oneOfs) { String refSchema = oneOf.get$ref(); if (refSchema != null) { - String name = getDiscriminatorValue(refSchema); + boolean hasProperty = findProperty(schema, discriminator.getPropertyName(), false, new HashSet<>()) != null; + String name = getDiscriminatorValue(refSchema, discriminator.getPropertyName(), hasProperty); mappings.put(name, refSchema); } } } - - + // remove oneOf and only keep the discriminator mapping schema.setOneOf(null); } + return schema; } - protected String getDiscriminatorValue(String refSchema) { - String schemaName = refSchema.contains("/") ? refSchema.substring(refSchema.lastIndexOf('/') + 1) : refSchema;; + private boolean isInlineSchema(Schema schema) { + if (openAPI.getComponents()!=null && openAPI.getComponents().getSchemas()!=null) { + int identity = System.identityHashCode(schema); + for (Schema componentSchema: openAPI.getComponents().getSchemas().values()) { + if (System.identityHashCode(componentSchema) == identity) { + return false; + } + } + } + return true; + } + + /** + * Best effort to retrieve a good discriminator value. + * By order of precedence: + *
    + *
  • x-discriminator-value
  • + *
  • single enum value for attribute used by the discriminator.propertyName
  • + *
  • hame of the schema
  • + *
+ * + * @param refSchema $ref value like #/components/schemas/Dog + * @param discriminatorPropertyName name of the property used in the discriminator mapping + * @param propertyAlreadyPresent if true, delete the property in the referenced schemas to avoid duplicates + * + * @return the name + */ + protected String getDiscriminatorValue(String refSchema, String discriminatorPropertyName, boolean propertyAlreadyPresent) { + String schemaName = ModelUtils.getSimpleRef(refSchema); Schema schema = ModelUtils.getSchema(openAPI, schemaName); + Schema property = findProperty(schema, discriminatorPropertyName, propertyAlreadyPresent, new HashSet<>()); if (schema != null && schema.getExtensions() != null) { Object discriminatorValue = schema.getExtensions().get("x-discriminator-value"); if (discriminatorValue != null) { return discriminatorValue.toString(); } } + + // find the discriminator value as a unique enum value + if (property != null) { + List enums = property.getEnum(); + if (enums != null && enums.size() == 1) { + return enums.get(0).toString(); + } + } + return schemaName; } + /** + * find a property under the schema. + * + * @param schema + * @param propertyName property to find + * @param toDelete if true delete the found property + * @param visitedSchemas avoid infinite recursion + * @return found property or null if not found. + */ + private Schema findProperty(Schema schema, String propertyName, boolean toDelete, HashSet visitedSchemas) { + if (propertyName == null || schema == null || visitedSchemas.contains(schema)) { + return null; + } + visitedSchemas.add(schema); + Map properties = schema.getProperties(); + if (properties != null) { + Schema property = properties.get(propertyName); + if (property != null) { + if (toDelete) { + if (schema.getProperties().remove(propertyName) != null) { + schema.setProperties(null); + } + } + return property; + } + } + List allOfs = schema.getAllOf(); + if (allOfs != null) { + for (Schema child : allOfs) { + Schema found = findProperty(child, propertyName, toDelete, visitedSchemas); + if (found != null) { + return found; + } + } + } + + return null; + } + - protected void ensureInheritance(Schema parent, String parentName) { + /** + * ensure that all schemas referenced in the discriminator mapping has an allOf to the parent schema. + * + * This allows DefaultCodeGen to detect inheritance. + * + * @param parent parent schma + * @param parentName name of the parent schema + */ + protected void ensureInheritanceForDiscriminatorMappings(Schema parent, String parentName) { Discriminator discriminator = parent.getDiscriminator(); if (discriminator != null && discriminator.getMapping() != null) { - for (String mapping : discriminator.getMapping().values()) { - String refSchemaName = getDiscriminatorValue(mapping); + String refSchemaName = ModelUtils.getSimpleRef(mapping); Schema child = ModelUtils.getSchema(openAPI, refSchemaName); if (child != null) { - ensureInheritance(parent, child, parentName); + if (parentName != null) { + ensureInheritanceForDiscriminatorMappings(parent, child, parentName, new HashSet<>()); + } } } } } - protected void ensureInheritance(Schema parent, Schema child, String parentName) { + /** + * If not already present, add in the child an allOf referencing the parent. + */ + protected void ensureInheritanceForDiscriminatorMappings(Schema parent, Schema child, String parentName, Set visitedSchemas) { String reference = "#/components/schemas/" + parentName; List allOf = child.getAllOf(); if (allOf != null) { - if (hasParent(child, parent, reference)) { + if (hasParent(parent, child, reference, visitedSchemas)) { + // already done, so no need to add return; } + Schema refToParent = new Schema<>().$ref(reference); + allOf.add(refToParent); } else { allOf = new ArrayList<>(); child.setAllOf(allOf); + Schema refToParent = new Schema<>().$ref(reference); + allOf.add(refToParent); + if (child.getProperties() != null) { + // move the properties inside the new allOf. + Schema childProperties = new Schema<>().properties(child.getProperties()); + allOf.add(childProperties); + child.setProperties(null); + child.setType(null); + } } - Schema refToParent = new Schema<>().$ref(reference); - allOf.add(refToParent); } - private boolean hasParent(Schema child, Schema parent, String reference) { + /** + * return true if the child as an allOf referencing the parent scham. + */ + private boolean hasParent(Schema parent, Schema child, String reference, Set visitedSchemas) { if (child.get$ref() != null && child.get$ref().equals(reference)) { return true; } List allOf = child.getAllOf(); if (allOf != null) { - return allOf.stream().anyMatch(s -> hasParent(s, parent, reference)); + for (Schema schema : allOf) { + if (visitedSchemas.contains(schema)) { + return false; + } + visitedSchemas.add(schema); + if (hasParent(schema, parent, reference, visitedSchemas)) { + return true; + } + } } return false; } 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 84d8cf484f73..de529b43f976 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 @@ -17,8 +17,10 @@ package org.openapitools.codegen.utils; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.util.Yaml; import io.swagger.v3.core.util.AnnotationsUtils; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; @@ -2645,4 +2647,20 @@ public LinkedHashSet build() { } } } + + + /* + * Simplest dump of an openApi contract on the console. + * + * Only use for debugging. + */ + public static void dumpAsYaml(OpenAPI openAPI) { + ObjectMapper mapper = Yaml.mapper(); + try { + String yaml = mapper.writeValueAsString(openAPI); + System.out.println(yaml); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 265b743d79bc..a2911fd45e73 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -1507,7 +1507,6 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { @Test public void testREPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING() { - // to test array schema processing in 3.1 spec OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/oneOf_issue_23527.yaml"); Map inputRules = Map.of("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true"); @@ -1519,5 +1518,30 @@ public void testREPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING() { assertEquals(mapping, Map.of("MultiPolygon", "#/components/schemas/Multi-Polygon", "Polygon", "#/components/schemas/Polygon" )); } + @Test + public void issue_14769() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/oneOf_issue_14769.yaml"); + Map inputRules = Map.of("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true"); + OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, inputRules); + openAPINormalizer.normalize(); +// ModelUtils.dumpAsYaml(openAPI); + Schema vehicle = openAPI.getComponents().getSchemas().get("Vehicle"); + Map mapping = vehicle.getDiscriminator().getMapping(); + assertEquals(mapping, Map.of("car", "#/components/schemas/Car", "plane", "#/components/schemas/Plane" )); + Schema car = openAPI.getComponents().getSchemas().get("Car"); + assertFalse(car.getProperties().containsKey("type")); + } + + @Test + public void oneOf_issue_23276() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/oneOf_issue_23276.yaml"); + Map inputRules = Map.of("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true"); + OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, inputRules); + openAPINormalizer.normalize(); +// ModelUtils.dumpAsYaml(openAPI); + Schema payload = (Schema)openAPI.getComponents().getSchemas().get("DeviceLifecycleEvent").getProperties().get("payload"); + // inline oneOf are not converted + assertNotNull(payload.getOneOf()); + } } 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 2142dec9e289..09cd1739c501 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 @@ -72,7 +72,6 @@ import static org.openapitools.codegen.CodegenConstants.*; import static org.openapitools.codegen.TestUtils.*; import static org.openapitools.codegen.languages.JavaClientCodegen.*; -import static org.openapitools.codegen.languages.SpringCodegen.SPRING_BOOT; import static org.testng.Assert.*; public class JavaClientCodegenTest { @@ -4371,26 +4370,6 @@ void issue_912() { .containsWithName("JsonSubTypes") .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Folder.class", "name", "\"folder\"")) .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Source.class", "name", "\"source\"")); - - } - - @Test - void issue_19261() { - Map files = generateFromContract("src/test/resources/3_0/oneOf_issue_19261.yaml", APACHE, - Map.of(), - codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true") - .addInlineSchemaOption("REFACTOR_ALLOF_INLINE_SCHEMAS", "true")); - JavaFileAssert.assertThat(files.get("Product.java")) - .isNormalClass() - .doesNotExtendsClasses() - .fileContains("AboType type") - .assertTypeAnnotations() - .containsWithNameAndAttributes("JsonTypeInfo", Map.of("include", "JsonTypeInfo.As.PROPERTY", "property", "\"type\"")) - .containsWithName("JsonSubTypes") - .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "HomeProduct.class", "name", "\"home\"")) - .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "InternetProduct.class", "name", "\"internet\"")); - JavaFileAssert.assertThat(files.get("InternetProduct.java")) - .extendsClass("Product"); } } 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 7ad44780ef18..5a806f25be22 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 @@ -6762,4 +6762,12 @@ void oneof_polymorphism_and_inheritance() throws IOException { .isNormalClass() .extendsClass("FruitDto"); } + + @Test + public void issue_14769() throws IOException { + Map files = generateFromContract("src/test/resources/3_0/oneOf_issue_14769.yaml", SPRING_BOOT, + Map.of(MODEL_NAME_SUFFIX, "Dto", + GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true), + codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_14769.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_14769.yaml new file mode 100644 index 000000000000..02b6d52fc02a --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_14769.yaml @@ -0,0 +1,45 @@ +openapi: 3.0.1 +info: + title: discriminator_enums + version: '1.0.0' +components: + schemas: + Vehicle: + type: object + required: + - id + - type + properties: + id: + type: integer + type: + type: string + model: + type: string + name: + type: string + oneOf: + - $ref: '#/components/schemas/Car' + - $ref: '#/components/schemas/Plane' + discriminator: + propertyName: type + + Car: + type: object + properties: + type: + enum: + - car + has_4_wheel_drive: + type: boolean + + Plane: + type: object + properties: + type: + enum: + - plane + has_reactor: + type: boolean + nb_passengers: + type: integer \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23276.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23276.yaml new file mode 100644 index 000000000000..e42904e90ec8 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23276.yaml @@ -0,0 +1,120 @@ +openapi: 3.0.3 +info: + version: v1 + title: Device service events +paths: {} +components: + schemas: + DeviceLifecycleEvent: + type: object + description: Notification about device lifecycle events + required: + - type + - header + - payload + properties: + type: + type: string + enum: + - device_datatransfer_status_changed + - device_communication_mode_status_changed + header: + $ref: "#/components/schemas/DeviceMessageHeader" + payload: +# $ref: "#/components/schemas/DeviceLifecycleEventPayload" +# DeviceLifecycleEventPayload: + oneOf: + - $ref: "#/components/schemas/DeviceDataTransferEventPayload" + - $ref: "#/components/schemas/DeviceCommModeStatusEventPayload" + discriminator: + propertyName: type + mapping: + device_datatransfer_status_changed: "#/components/schemas/DeviceDataTransferEventPayload" + device_communication_mode_status_changed: "#/components/schemas/DeviceCommModeStatusEventPayload" + + DeviceMessageHeader: + type: object + required: + - ts + - correlationId + - deviceId + properties: + correlationId: + type: string + deviceId: + type: string + ts: + type: string + format: date-time + + DeviceDataTransferEventPayload: + type: object + required: + - deviceId + - serialNumber + - updatedAt + - eventData + properties: + deviceId: + type: string + serialNumber: + type: string + updatedAt: + type: string + eventData: + $ref: "#/components/schemas/DeviceDataTransferEventData" + + DeviceCommModeStatusEventPayload: + type: object + required: + - deviceId + - serialNumber + - updatedAt + - eventData + properties: + deviceId: + type: string + serialNumber: + type: string + updatedAt: + type: string + eventData: + $ref: "#/components/schemas/DeviceCommModeStatusEventData" + + DeviceDataTransferEventData: + type: object + required: + - StatusDetails + properties: + StatusDetails: + type: object + properties: + dataTransfer: + type: object + properties: + status: + type: string + enum: [capable, incapable, Not Applicable] + statusReason: + type: string + updatedAt: + type: string + isMuted: + type: boolean + + DeviceCommModeStatusEventData: + type: object + required: + - StatusDetails + properties: + StatusDetails: + type: object + properties: + communicationMode: + type: object + properties: + runtimeCommMode: + type: string + enum: [limited, normal] + updatedAt: + type: string \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23527_1.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23527_1.yaml index d212feddcb84..3d1fb4603b8e 100644 --- a/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23527_1.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_23527_1.yaml @@ -21,24 +21,20 @@ components: - $ref: '#/components/schemas/Polygon' - $ref: '#/components/schemas/MultiPolygon' Polygon: - allOf: - - $ref: '#/components/schemas/GeoJsonObject' - - type: object - properties: - coordinates: - type: array - items: - type: number - format: double + type: object + properties: + coordinates: + type: array + items: + type: number + format: double MultiPolygon: - allOf: - - $ref: '#/components/schemas/GeoJsonObject' - - type: object - properties: - coordinates: - type: array - items: - type: array - items: - type: number - format: double \ No newline at end of file + type: object + properties: + coordinates: + type: array + items: + type: array + items: + type: number + format: double \ No newline at end of file From 53c6f010b9968f61eda64babde1cf96afc79b54c Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 18 Apr 2026 16:36:31 +0200 Subject: [PATCH 12/22] Fix building of allOf --- .../org/openapitools/codegen/OpenAPINormalizer.java | 9 +++++---- .../openapitools/codegen/OpenAPINormalizerTest.java | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index fbe5b75b0ccf..70a1a6dd379a 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1690,7 +1690,7 @@ private Schema findProperty(Schema schema, String propertyName, boolean toDelete Schema property = properties.get(propertyName); if (property != null) { if (toDelete) { - if (schema.getProperties().remove(propertyName) != null) { + if (schema.getProperties().remove(propertyName) != null && schema.getProperties().isEmpty()) { schema.setProperties(null); } } @@ -1752,10 +1752,11 @@ protected void ensureInheritanceForDiscriminatorMappings(Schema parent, Schema c child.setAllOf(allOf); Schema refToParent = new Schema<>().$ref(reference); allOf.add(refToParent); - if (child.getProperties() != null) { + Map childProperties = child.getProperties(); + if (childProperties != null) { // move the properties inside the new allOf. - Schema childProperties = new Schema<>().properties(child.getProperties()); - allOf.add(childProperties); + Schema newChildProperties = new Schema<>().properties(childProperties); + allOf.add(newChildProperties); child.setProperties(null); child.setType(null); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index a2911fd45e73..2e25bb53b85c 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -16,9 +16,6 @@ package org.openapitools.codegen; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.swagger.util.Yaml; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; @@ -29,7 +26,6 @@ import org.openapitools.codegen.utils.ModelUtils; import org.testng.annotations.Test; -import java.lang.reflect.Array; import java.math.BigDecimal; import java.util.*; @@ -1529,7 +1525,11 @@ public void issue_14769() { Map mapping = vehicle.getDiscriminator().getMapping(); assertEquals(mapping, Map.of("car", "#/components/schemas/Car", "plane", "#/components/schemas/Plane" )); Schema car = openAPI.getComponents().getSchemas().get("Car"); - assertFalse(car.getProperties().containsKey("type")); + assertNull(car.getProperties()); + assertEquals(car.getAllOf().size(), 2); + assertEquals(((Schema)car.getAllOf().get(0)).get$ref(), "#/components/schemas/Vehicle"); + assertEquals(((Schema)car.getAllOf().get(1)).getProperties().size(), 1); + assertEquals(((Schema)car.getAllOf().get(1)).getProperties().keySet(), Set.of("has_4_wheel_drive")); } @Test From 18d2f8129b28d7afd9ddde71be5a1febcc23e47b Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 18 Apr 2026 16:50:23 +0200 Subject: [PATCH 13/22] Fix hasParent --- .../java/org/openapitools/codegen/OpenAPINormalizer.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 70a1a6dd379a..245741ddd8ad 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1767,6 +1767,9 @@ protected void ensureInheritanceForDiscriminatorMappings(Schema parent, Schema c * return true if the child as an allOf referencing the parent scham. */ private boolean hasParent(Schema parent, Schema child, String reference, Set visitedSchemas) { + if (child == null) { + return false; + } if (child.get$ref() != null && child.get$ref().equals(reference)) { return true; } @@ -1777,7 +1780,7 @@ private boolean hasParent(Schema parent, Schema child, String reference, Set Date: Sat, 18 Apr 2026 18:51:57 +0200 Subject: [PATCH 14/22] Fix some cubic findings --- docs/customization.md | 2 +- .../codegen/OpenAPINormalizer.java | 22 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 16f197ec588d..5981132b6519 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -651,7 +651,7 @@ Example: java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/required-properties.yaml -o /tmp/java-okhttp/ --openapi-normalizer REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT=true ``` -- `REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING``: when set to true, oneOf is removed and is converted into mappings in a discriminator mapping. +- `REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING`: when set to true, oneOf is removed and is converted into mappings in a discriminator mapping. Example: ``` diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 245741ddd8ad..a0325f7351a9 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1591,7 +1591,7 @@ protected Schema processSimplifyOneOf(Schema schema) { * */ protected Schema processReplaceOneOfByMapping(Schema schema) { - if (!getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) { + if (!getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING) || schema.getOneOf() == null) { return schema; } Discriminator discriminator = schema.getDiscriminator(); @@ -1606,7 +1606,7 @@ protected Schema processReplaceOneOfByMapping(Schema schema) { Map mappings = new TreeMap<>(); discriminator.setMapping(mappings); List oneOfs = schema.getOneOf(); - for (Schema oneOf: oneOfs) { + for (Schema oneOf : oneOfs) { String refSchema = oneOf.get$ref(); if (refSchema != null) { boolean hasProperty = findProperty(schema, discriminator.getPropertyName(), false, new HashSet<>()) != null; @@ -1614,9 +1614,16 @@ protected Schema processReplaceOneOfByMapping(Schema schema) { mappings.put(name, refSchema); } } + // remove oneOf and only keep the new discriminator mapping + schema.setOneOf(null); + } else if (discriminator.getPropertyName() == null) { + LOGGER.warn("Missing property name in discriminator"); + } else if (discriminator.getMapping() != null && discriminator.getMapping().size() != schema.getOneOf().size()) { + LOGGER.warn("Discriminator Mapping size " + discriminator.getMapping().size() + " mismatch with oneOf size " + schema.getOneOf().size()); + } else { + // remove oneOf and only keep the discriminator mapping + schema.setOneOf(null); } - // remove oneOf and only keep the discriminator mapping - schema.setOneOf(null); } return schema; @@ -1690,8 +1697,11 @@ private Schema findProperty(Schema schema, String propertyName, boolean toDelete Schema property = properties.get(propertyName); if (property != null) { if (toDelete) { - if (schema.getProperties().remove(propertyName) != null && schema.getProperties().isEmpty()) { - schema.setProperties(null); + if (schema.getProperties().remove(propertyName) != null) { + LOGGER.info("property " + propertyName + " has been removed in REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING normalization"); + if (schema.getProperties().isEmpty()) { + schema.setProperties(null); + } } } return property; From d3e23bb3b39b314d2e44b0b6f19abf5121d1e258 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 18 Apr 2026 18:57:24 +0200 Subject: [PATCH 15/22] Fix some cubic findings --- .../java/org/openapitools/codegen/OpenAPINormalizer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index a0325f7351a9..494716483e31 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1774,12 +1774,13 @@ protected void ensureInheritanceForDiscriminatorMappings(Schema parent, Schema c } /** - * return true if the child as an allOf referencing the parent scham. + * return true if the child as an allOf referencing the parent schema. */ private boolean hasParent(Schema parent, Schema child, String reference, Set visitedSchemas) { - if (child == null) { + if (child == null || visitedSchemas.contains(child)) { return false; } + visitedSchemas.add(child); if (child.get$ref() != null && child.get$ref().equals(reference)) { return true; } From 725a0ae280306f8b318c91a3d639d3ca824e4ddd Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 18 Apr 2026 19:06:13 +0200 Subject: [PATCH 16/22] Fix infinite recursion stopping too early --- .../main/java/org/openapitools/codegen/OpenAPINormalizer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 494716483e31..9f2b91216172 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1790,7 +1790,6 @@ private boolean hasParent(Schema parent, Schema child, String reference, Set Date: Sat, 18 Apr 2026 20:12:17 +0200 Subject: [PATCH 17/22] Force build --- .../org/openapitools/codegen/java/JavaClientCodegenTest.java | 1 - 1 file changed, 1 deletion(-) 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 09cd1739c501..f703055df421 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 @@ -4352,7 +4352,6 @@ void replaceOneOfByDiscriminatorMapping(String file) { .extendsClass("GeoJsonObject") .doesNotImplementInterfaces("GeoJsonObject") .fileContains("List coordinates"); - } @Test From befa7fa8231abaecd2e7c987df6caf6a8ef0bc0a Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 20 Apr 2026 09:10:46 +0200 Subject: [PATCH 18/22] Use getReferencedSchema in search for properties --- .../codegen/OpenAPINormalizer.java | 31 ++++++++++++------- .../codegen/java/JavaClientCodegenTest.java | 2 +- .../java/spring/SpringCodegenTest.java | 9 +++--- .../test/resources/3_0/oneOf_issue_14769.yaml | 20 +++++++----- 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 9f2b91216172..a48a63f57196 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1603,25 +1603,31 @@ protected Schema processReplaceOneOfByMapping(Schema schema) { return schema; } if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) { + List oneOfs = schema.getOneOf(); + if (oneOfs.stream().anyMatch(oneOf -> oneOf.get$ref() == null)) { + LOGGER.warn("oneOf should only contain $ref for REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING normalization"); + return schema; + } Map mappings = new TreeMap<>(); + // is the discriminator qttribute qlready in this schema? + // if yes, it will be deleted in references oneOf to avoid duplicates + boolean hasProperty = findProperty(schema, discriminator.getPropertyName(), false, new HashSet<>()) != null; discriminator.setMapping(mappings); - List oneOfs = schema.getOneOf(); for (Schema oneOf : oneOfs) { String refSchema = oneOf.get$ref(); - if (refSchema != null) { - boolean hasProperty = findProperty(schema, discriminator.getPropertyName(), false, new HashSet<>()) != null; - String name = getDiscriminatorValue(refSchema, discriminator.getPropertyName(), hasProperty); - mappings.put(name, refSchema); - } + String name = getDiscriminatorValue(refSchema, discriminator.getPropertyName(), hasProperty, new HashSet<>(List.of(schema))); + mappings.put(name, refSchema); + } // remove oneOf and only keep the new discriminator mapping schema.setOneOf(null); } else if (discriminator.getPropertyName() == null) { LOGGER.warn("Missing property name in discriminator"); } else if (discriminator.getMapping() != null && discriminator.getMapping().size() != schema.getOneOf().size()) { - LOGGER.warn("Discriminator Mapping size " + discriminator.getMapping().size() + " mismatch with oneOf size " + schema.getOneOf().size()); + LOGGER.warn("Discriminator mapping size " + discriminator.getMapping().size() + " mismatch with oneOf size " + schema.getOneOf().size()); } else { // remove oneOf and only keep the discriminator mapping + LOGGER.info("Removing oneOf, discriminator mapping takes precedences on OneOfs"); schema.setOneOf(null); } } @@ -1656,10 +1662,10 @@ private boolean isInlineSchema(Schema schema) { * * @return the name */ - protected String getDiscriminatorValue(String refSchema, String discriminatorPropertyName, boolean propertyAlreadyPresent) { + protected String getDiscriminatorValue(String refSchema, String discriminatorPropertyName, boolean propertyAlreadyPresent, Set visitedSchemas) { String schemaName = ModelUtils.getSimpleRef(refSchema); Schema schema = ModelUtils.getSchema(openAPI, schemaName); - Schema property = findProperty(schema, discriminatorPropertyName, propertyAlreadyPresent, new HashSet<>()); + Schema property = findProperty(schema, discriminatorPropertyName, propertyAlreadyPresent, visitedSchemas); if (schema != null && schema.getExtensions() != null) { Object discriminatorValue = schema.getExtensions().get("x-discriminator-value"); if (discriminatorValue != null) { @@ -1668,6 +1674,7 @@ protected String getDiscriminatorValue(String refSchema, String discriminatorPro } // find the discriminator value as a unique enum value + property = ModelUtils.getReferencedSchema(openAPI, property); if (property != null) { List enums = property.getEnum(); if (enums != null && enums.size() == 1) { @@ -1687,14 +1694,15 @@ protected String getDiscriminatorValue(String refSchema, String discriminatorPro * @param visitedSchemas avoid infinite recursion * @return found property or null if not found. */ - private Schema findProperty(Schema schema, String propertyName, boolean toDelete, HashSet visitedSchemas) { + private Schema findProperty(Schema schema, String propertyName, boolean toDelete, Set visitedSchemas) { + schema = ModelUtils.getReferencedSchema(openAPI, schema); if (propertyName == null || schema == null || visitedSchemas.contains(schema)) { return null; } visitedSchemas.add(schema); Map properties = schema.getProperties(); if (properties != null) { - Schema property = properties.get(propertyName); + Schema property = ModelUtils.getReferencedSchema(openAPI, properties.get(propertyName)); if (property != null) { if (toDelete) { if (schema.getProperties().remove(propertyName) != null) { @@ -1784,6 +1792,7 @@ private boolean hasParent(Schema parent, Schema child, String reference, Set allOf = child.getAllOf(); if (allOf != null) { for (Schema schema : allOf) { 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 f703055df421..463bf66f4ed8 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 @@ -4355,7 +4355,7 @@ void replaceOneOfByDiscriminatorMapping(String file) { } @Test - void issue_912() { + void oneOf_issue_912() { Map files = generateFromContract("src/test/resources/3_0/java/issue_912.yaml", APACHE, Map.of(), codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true") 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 5a806f25be22..5fbea6653c24 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 @@ -6684,13 +6684,14 @@ void replaceOneOfByDiscriminatorMapping(String file) throws IOException { JavaFileAssert.assertThat(files.get("Polygon.java")) .extendsClass("GeoJsonObject") + .fileDoesNotContain(" type;") .doesNotImplementInterfaces("GeoJsonObject") .fileContains("List coordinates") .fileDoesNotContain("@JsonSubTypes"); } @Test - void issue_19261() throws IOException { + void oneOf_issue_19261() throws IOException { Map files = generateFromContract("src/test/resources/3_0/oneOf_issue_19261.yaml", SPRING_BOOT, Map.of(GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true), codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); @@ -6708,7 +6709,7 @@ void issue_19261() throws IOException { } @Test - void issue_22013() throws IOException { + void oneOf_issue_22013() throws IOException { Map files = generateFromContract("src/test/resources/3_0/oneOf_issue_22013.yaml", SPRING_BOOT, Map.of(GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true), codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); @@ -6727,7 +6728,7 @@ void issue_22013() throws IOException { } @Test - void issue_23577() throws IOException { + void oneOf_issue_23577() throws IOException { Map files = generateFromContract("src/test/resources/3_0/oneOf_issue_23577.yaml", SPRING_BOOT, Map.of(GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true), codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); @@ -6764,7 +6765,7 @@ void oneof_polymorphism_and_inheritance() throws IOException { } @Test - public void issue_14769() throws IOException { + void oneOf_issue_14769() throws IOException { Map files = generateFromContract("src/test/resources/3_0/oneOf_issue_14769.yaml", SPRING_BOOT, Map.of(MODEL_NAME_SUFFIX, "Dto", GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true), diff --git a/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_14769.yaml b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_14769.yaml index 02b6d52fc02a..a11ff04c8826 100644 --- a/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_14769.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/oneOf_issue_14769.yaml @@ -35,11 +35,15 @@ components: Plane: type: object - properties: - type: - enum: - - plane - has_reactor: - type: boolean - nb_passengers: - type: integer \ No newline at end of file + allOf: + - properties: + type: + $ref: '#/components/schemas/PlaneEnum' + has_reactor: + type: boolean + nb_passengers: + type: integer + PlaneEnum: + type: string + enum: + - plane \ No newline at end of file From 5d2fab68e62008af43e6db42dbfa6b046b2dd970 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 20 Apr 2026 11:50:51 +0200 Subject: [PATCH 19/22] Improve hasParent -> isParentReferencedInChild --- .../openapitools/codegen/OpenAPINormalizer.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index a48a63f57196..5d2c6ca42254 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1759,7 +1759,7 @@ protected void ensureInheritanceForDiscriminatorMappings(Schema parent, Schema c String reference = "#/components/schemas/" + parentName; List allOf = child.getAllOf(); if (allOf != null) { - if (hasParent(parent, child, reference, visitedSchemas)) { + if (isParentReferencedInChild(parent, child, reference, visitedSchemas)) { // already done, so no need to add return; } @@ -1784,22 +1784,22 @@ protected void ensureInheritanceForDiscriminatorMappings(Schema parent, Schema c /** * return true if the child as an allOf referencing the parent schema. */ - private boolean hasParent(Schema parent, Schema child, String reference, Set visitedSchemas) { + private boolean isParentReferencedInChild(Schema parent, Schema child, String reference, Set visitedSchemas) { if (child == null || visitedSchemas.contains(child)) { return false; } - visitedSchemas.add(child); if (child.get$ref() != null && child.get$ref().equals(reference)) { return true; } child = ModelUtils.getReferencedSchema(openAPI, child); + if (visitedSchemas.contains(child)) { + return false; + } + visitedSchemas.add(child); List allOf = child.getAllOf(); if (allOf != null) { for (Schema schema : allOf) { - if (visitedSchemas.contains(schema)) { - return false; - } - if (hasParent(parent, schema, reference, visitedSchemas)) { + if (isParentReferencedInChild(parent, schema, reference, visitedSchemas)) { return true; } } From a6e30d4dbede0e636bfaeaa212e17e10a5dc1300 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 20 Apr 2026 13:12:31 +0200 Subject: [PATCH 20/22] Cubic suggestions --- .../codegen/OpenAPINormalizer.java | 5 ++- .../java/assertions/JavaFileAssert.java | 37 ++++++++++++------- .../java/spring/SpringCodegenTest.java | 10 +++++ 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 5d2c6ca42254..4a649b91a446 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1773,7 +1773,10 @@ protected void ensureInheritanceForDiscriminatorMappings(Schema parent, Schema c Map childProperties = child.getProperties(); if (childProperties != null) { // move the properties inside the new allOf. - Schema newChildProperties = new Schema<>().properties(childProperties); + Schema newChildProperties = new Schema<>() + .properties(childProperties) + .additionalProperties(child.getAdditionalProperties()); + ModelUtils.copyMetadata(child, newChildProperties); allOf.add(newChildProperties); child.setProperties(null); child.setType(null); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java index 5389a4de51a9..df14326125e6 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java @@ -2,6 +2,8 @@ import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; import com.github.javaparser.ast.body.*; import com.github.javaparser.ast.nodeTypes.NodeWithName; import com.github.javaparser.ast.nodeTypes.modifiers.NodeWithAbstractModifier; @@ -59,33 +61,40 @@ public JavaFileAssert isNormalClass() { return this; } - public JavaFileAssert extendsClass(String... parentClass) { - Set expectedClasses = Stream.of(parentClass) - .collect(Collectors.toSet()); - - Set actualParents = actual.getType(0) + public JavaFileAssert extendsClass(String parentClass) { + String actualParent = actual.getType(0) .asClassOrInterfaceDeclaration().getExtendedTypes() .stream() + .filter(JavaFileAssert::isClass) .map(ClassOrInterfaceType::getNameWithScope) - .collect(Collectors.toSet()); + .findFirst() + .orElse(null); - Assertions.assertThat(actualParents) + Assertions.assertThat(actualParent) .withFailMessage("Expected type %s to extends %s, but found %s", - actual.getType(0).getName().asString(), expectedClasses, actualParents) - .isEqualTo(expectedClasses); + actual.getType(0).getName().asString(), parentClass, actualParent) + .isEqualTo(parentClass); return this; } + private static boolean isClass(ClassOrInterfaceType cit) { + return cit.asClassOrInterfaceType().getParentNode() + .map(node -> node instanceof ClassOrInterfaceDeclaration && !((ClassOrInterfaceDeclaration)node).isInterface()) + .orElse(false); + } + public JavaFileAssert doesNotExtendsClasses() { - Set actualParents = actual.getType(0) + String actualParent = actual.getType(0) .asClassOrInterfaceDeclaration().getExtendedTypes() .stream() + .filter(JavaFileAssert::isClass) .map(ClassOrInterfaceType::getNameWithScope) - .collect(Collectors.toSet()); - Assertions.assertThat(actualParents) + .findFirst() + .orElse(null); + Assertions.assertThat(actualParent) .withFailMessage("Expected type %s to extends a class, but found %s", - actual.getType(0).getName().asString(), actualParents) - .isEmpty(); + actual.getType(0).getName().asString(), actualParent) + .isNull(); return this; } 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 5fbea6653c24..5d05199926c4 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 @@ -6770,5 +6770,15 @@ void oneOf_issue_14769() throws IOException { Map.of(MODEL_NAME_SUFFIX, "Dto", GENERATE_MODEL_DOCS, false, GENERATE_APIS, false, INTERFACE_ONLY, true), codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true")); + + JavaFileAssert.assertThat(files.get("VehicleDto.java")) + .isNormalClass() + .assertTypeAnnotations().containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "CarDto.class", "name", "\"car\"")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "PlaneDto.class", "name", "\"plane\"")); + + JavaFileAssert.assertThat(files.get("CarDto.java")) + .isNormalClass() + .extendsClass("VehicleDto"); } } From 59b1c26f233456b66f5978bc4b8773239cde26f9 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 20 Apr 2026 17:37:35 +0200 Subject: [PATCH 21/22] Add assertions for JsonSubTypes.Types --- .../org/openapitools/codegen/java/JavaClientCodegenTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 463bf66f4ed8..41a010f1db54 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 @@ -4346,7 +4346,9 @@ void replaceOneOfByDiscriminatorMapping(String file) { .fileContains("String type") .fileDoesNotContain("coordinates") .assertTypeAnnotations() - .containsWithName("JsonSubTypes"); + .containsWithName("JsonSubTypes") + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "MultiPolygon.class", "name", "\"MultiPolygon\"")) + .recursivelyContainsWithNameAndAttributes("JsonSubTypes.Type", Map.of("value", "Polygon.class", "name", "\"Polygon\"")); JavaFileAssert.assertThat(files.get("Polygon.java")) .extendsClass("GeoJsonObject") From 0facdf72c058da03da8f351c72291fb45b35679b Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 20 Apr 2026 17:51:31 +0200 Subject: [PATCH 22/22] Clean moved child --- .../openapitools/codegen/OpenAPINormalizer.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 4a649b91a446..cb010967e826 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -1745,7 +1745,7 @@ protected void ensureInheritanceForDiscriminatorMappings(Schema parent, String p Schema child = ModelUtils.getSchema(openAPI, refSchemaName); if (child != null) { if (parentName != null) { - ensureInheritanceForDiscriminatorMappings(parent, child, parentName, new HashSet<>()); + ensureInheritanceForDiscriminatorMapping(parent, child, parentName, new HashSet<>()); } } } @@ -1755,7 +1755,7 @@ protected void ensureInheritanceForDiscriminatorMappings(Schema parent, String p /** * If not already present, add in the child an allOf referencing the parent. */ - protected void ensureInheritanceForDiscriminatorMappings(Schema parent, Schema child, String parentName, Set visitedSchemas) { + protected void ensureInheritanceForDiscriminatorMapping(Schema parent, Schema child, String parentName, Set visitedSchemas) { String reference = "#/components/schemas/" + parentName; List allOf = child.getAllOf(); if (allOf != null) { @@ -1778,8 +1778,17 @@ protected void ensureInheritanceForDiscriminatorMappings(Schema parent, Schema c .additionalProperties(child.getAdditionalProperties()); ModelUtils.copyMetadata(child, newChildProperties); allOf.add(newChildProperties); - child.setProperties(null); - child.setType(null); + child.properties(null) + .type(null) + .additionalProperties(null) + .description(null) + ._default(null) + .deprecated(null) + .example(null) + .examples(null) + .readOnly(null) + .writeOnly(null) + .title(null); } } }