Skip to content

Commit bf1dc5f

Browse files
committed
normalizer REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING
1 parent 045cfde commit bf1dc5f

10 files changed

Lines changed: 295 additions & 2 deletions

File tree

docs/customization.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,13 @@ Example:
651651
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
652652
```
653653
654+
- `REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING``: when set to true, oneOf is removed and is converted into mappings in a discriminator mapping.
655+
656+
Example:
657+
```
658+
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
659+
```
660+
654661
- `FILTER`
655662
656663
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.

modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ public class OpenAPINormalizer {
7878
// are removed as most generators cannot handle such case at the moment
7979
final String REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY = "REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY";
8080

81+
// when set to true, oneOf is removed and is converted into mappings in a discriminator mapping
82+
final String REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING = "REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING";
83+
84+
8185
// when set to true, oneOf/anyOf with either string or enum string as sub schemas will be simplified
8286
// to just string
8387
final String SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING = "SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING";
@@ -214,6 +218,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
214218
ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM);
215219
ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT);
216220
ruleNames.add(SORT_MODEL_PROPERTIES);
221+
ruleNames.add(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING);
217222

218223
// rules that are default to true
219224
rules.put(SIMPLIFY_ONEOF_ANYOF, true);
@@ -1053,6 +1058,8 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
10531058
// simplify first as the schema may no longer be a oneOf after processing the rule below
10541059
schema = processSimplifyOneOf(schema);
10551060

1061+
schema = processReplaceOneOfByMapping(schema);
1062+
10561063
// if it's still a oneOf, loop through the sub-schemas
10571064
if (schema.getOneOf() != null) {
10581065
for (int i = 0; i < schema.getOneOf().size(); i++) {
@@ -1569,6 +1576,33 @@ protected Schema processSimplifyOneOf(Schema schema) {
15691576
return schema;
15701577
}
15711578

1579+
1580+
protected Schema processReplaceOneOfByMapping(Schema schema) {
1581+
if (!getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) {
1582+
return schema;
1583+
}
1584+
1585+
if (schema.getDiscriminator() != null) {
1586+
Discriminator discriminator = schema.getDiscriminator();
1587+
if (discriminator.getMapping() == null) {
1588+
Map<String, String> mapping = new TreeMap<>();
1589+
discriminator.setMapping(mapping);
1590+
for (Object oneOfObject : schema.getOneOf()) {
1591+
Schema oneOf = (Schema) oneOfObject;
1592+
String ref = oneOf.get$ref();
1593+
if (ref != null) {
1594+
String name = ref.contains("/") ? ref.substring(ref.lastIndexOf('/') + 1) : ref;
1595+
mapping.put(name, oneOf.get$ref());
1596+
}
1597+
}
1598+
}
1599+
1600+
schema.setOneOf(null);
1601+
}
1602+
return schema;
1603+
}
1604+
1605+
15721606
/**
15731607
* Set nullable to true in array/set if needed.
15741608
*

modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package org.openapitools.codegen;
1818

19+
import com.fasterxml.jackson.core.JsonProcessingException;
20+
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import io.swagger.util.Yaml;
1922
import io.swagger.v3.oas.models.OpenAPI;
2023
import io.swagger.v3.oas.models.Operation;
2124
import io.swagger.v3.oas.models.PathItem;
@@ -1502,4 +1505,29 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
15021505
}
15031506
}
15041507

1508+
@Test
1509+
public void testREPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING() {
1510+
// to test array schema processing in 3.1 spec
1511+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/spring/issue_23527.yaml");
1512+
1513+
Map<String, String> inputRules = Map.of("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true");
1514+
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, inputRules);
1515+
openAPINormalizer.normalize();
1516+
dump(openAPI);
1517+
}
1518+
1519+
private void dump(OpenAPI openAPI) {
1520+
1521+
ObjectMapper mapper = Yaml.mapper();
1522+
String yaml = null;
1523+
try {
1524+
yaml = mapper.writeValueAsString(openAPI);
1525+
} catch (JsonProcessingException e) {
1526+
throw new RuntimeException(e);
1527+
}
1528+
1529+
System.out.println(yaml);
1530+
1531+
}
1532+
15051533
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
import static org.openapitools.codegen.CodegenConstants.*;
7373
import static org.openapitools.codegen.TestUtils.*;
7474
import static org.openapitools.codegen.languages.JavaClientCodegen.*;
75+
import static org.openapitools.codegen.languages.SpringCodegen.SPRING_BOOT;
7576
import static org.testng.Assert.*;
7677

7778
public class JavaClientCodegenTest {
@@ -4325,4 +4326,33 @@ public void testJspecify(String library, boolean useSpringBoot4, boolean hasJspe
43254326
.fileContains("@org.jspecify.annotations.NullMarked");
43264327

43274328
}
4329+
4330+
@DataProvider(name = "replaceOneOf")
4331+
public Object[][] replaceOneOf() {
4332+
return new Object[][]{
4333+
{"src/test/resources/3_0/spring/issue_23527.yaml"},
4334+
{"src/test/resources/3_0/spring/issue_23527_1.yaml"},
4335+
{"src/test/resources/3_0/spring/issue_23527_2.yaml"}
4336+
};
4337+
}
4338+
4339+
@Test(dataProvider = "replaceOneOf" )
4340+
void replaceOneOfByDiscriminatorMapping(String file) throws IOException {
4341+
Map<String, File> files = generateFromContract(file, APACHE, Map.of(),
4342+
codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true"));
4343+
4344+
JavaFileAssert.assertThat(files.get("GeoJsonObject.java"))
4345+
.isNormalClass()
4346+
.doesNotExtendsClasses()
4347+
.fileContains("String type")
4348+
.fileDoesNotContain("coordinates")
4349+
.assertTypeAnnotations()
4350+
.containsWithName("JsonSubTypes");
4351+
4352+
JavaFileAssert.assertThat(files.get("Polygon.java"))
4353+
.extendsClass("GeoJsonObject")
4354+
.doesNotImplementInterfaces("GeoJsonObject")
4355+
.fileContains("List<Double> coordinates");
4356+
4357+
}
43284358
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/assertions/JavaFileAssert.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,36 @@ public JavaFileAssert isNormalClass() {
5959
return this;
6060
}
6161

62+
public JavaFileAssert extendsClass(String... parentClass) {
63+
Set<String> expectedClasses = Stream.of(parentClass)
64+
.collect(Collectors.toSet());
65+
66+
Set<String> actualParents = actual.getType(0)
67+
.asClassOrInterfaceDeclaration().getExtendedTypes()
68+
.stream()
69+
.map(ClassOrInterfaceType::getNameWithScope)
70+
.collect(Collectors.toSet());
71+
72+
Assertions.assertThat(actualParents)
73+
.withFailMessage("Expected type %s to extends %s, but found %s",
74+
actual.getType(0).getName().asString(), expectedClasses, actualParents)
75+
.isEqualTo(expectedClasses);
76+
return this;
77+
}
78+
79+
public JavaFileAssert doesNotExtendsClasses() {
80+
Set<String> actualParents = actual.getType(0)
81+
.asClassOrInterfaceDeclaration().getExtendedTypes()
82+
.stream()
83+
.map(ClassOrInterfaceType::getNameWithScope)
84+
.collect(Collectors.toSet());
85+
Assertions.assertThat(actualParents)
86+
.withFailMessage("Expected type %s to extends a class, but found %s",
87+
actual.getType(0).getName().asString(), actualParents)
88+
.isEmpty();
89+
return this;
90+
}
91+
6292
public JavaFileAssert implementsInterfaces(String... implementedInterfaces) {
6393
Set<String> expectedInterfaces = Stream.of(implementedInterfaces)
6494
.collect(Collectors.toSet());

modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6664,4 +6664,35 @@ public void testJspecify(String library, int springBootVersion, String fooApiFil
66646664
JavaFileAssert.assertThat(files.get("model/package-info.java"))
66656665
.fileContains("@org.jspecify.annotations.NullMarked");
66666666
}
6667+
6668+
6669+
@DataProvider(name = "replaceOneOf")
6670+
public Object[][] replaceOneOf() {
6671+
return new Object[][]{
6672+
{"src/test/resources/3_0/spring/issue_23527.yaml"},
6673+
{"src/test/resources/3_0/spring/issue_23527_1.yaml"},
6674+
{"src/test/resources/3_0/spring/issue_23527_2.yaml"}
6675+
};
6676+
}
6677+
6678+
@Test(dataProvider = "replaceOneOf" )
6679+
void replaceOneOfByDiscriminatorMapping(String file) throws IOException {
6680+
Map<String, File> files = generateFromContract(file, SPRING_BOOT, Map.of(),
6681+
codegen -> codegen.addOpenapiNormalizer("REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING", "true"));
6682+
6683+
JavaFileAssert.assertThat(files.get("GeoJsonObject.java"))
6684+
.isNormalClass()
6685+
.doesNotExtendsClasses()
6686+
.fileContains("String type")
6687+
.fileDoesNotContain("coordinates")
6688+
.assertTypeAnnotations()
6689+
.containsWithName("JsonSubTypes")
6690+
;
6691+
6692+
JavaFileAssert.assertThat(files.get("Polygon.java"))
6693+
.extendsClass("GeoJsonObject")
6694+
.doesNotImplementInterfaces("GeoJsonObject")
6695+
.fileContains("List<Double> coordinates");
6696+
6697+
}
66676698
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
openapi: 3.0.3
2+
info:
3+
title: GeoJSON Discriminator Test
4+
version: 1.0.0
5+
paths: {}
6+
components:
7+
schemas:
8+
GeoJsonObject:
9+
type: object
10+
properties:
11+
type:
12+
type: string
13+
required:
14+
- type
15+
discriminator:
16+
propertyName: type
17+
oneOf:
18+
- $ref: '#/components/schemas/Polygon'
19+
- $ref: '#/components/schemas/MultiPolygon'
20+
Polygon:
21+
allOf:
22+
- $ref: '#/components/schemas/GeoJsonObject'
23+
- type: object
24+
properties:
25+
coordinates:
26+
type: array
27+
items:
28+
type: number
29+
format: double
30+
MultiPolygon:
31+
allOf:
32+
- $ref: '#/components/schemas/GeoJsonObject'
33+
- type: object
34+
properties:
35+
coordinates:
36+
type: array
37+
items:
38+
type: array
39+
items:
40+
type: number
41+
format: double
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
openapi: 3.0.3
2+
info:
3+
title: GeoJSON Discriminator Test
4+
version: 1.0.0
5+
paths: {}
6+
components:
7+
schemas:
8+
GeoJsonObject:
9+
type: object
10+
properties:
11+
type:
12+
type: string
13+
required:
14+
- type
15+
discriminator:
16+
propertyName: type
17+
mapping:
18+
Polygon: '#/components/schemas/Polygon'
19+
MultiPolygon: '#/components/schemas/MultiPolygon'
20+
oneOf:
21+
- $ref: '#/components/schemas/Polygon'
22+
- $ref: '#/components/schemas/MultiPolygon'
23+
Polygon:
24+
allOf:
25+
- $ref: '#/components/schemas/GeoJsonObject'
26+
- type: object
27+
properties:
28+
coordinates:
29+
type: array
30+
items:
31+
type: number
32+
format: double
33+
MultiPolygon:
34+
allOf:
35+
- $ref: '#/components/schemas/GeoJsonObject'
36+
- type: object
37+
properties:
38+
coordinates:
39+
type: array
40+
items:
41+
type: array
42+
items:
43+
type: number
44+
format: double
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
openapi: 3.0.3
2+
info:
3+
title: GeoJSON Discriminator Test
4+
version: 1.0.0
5+
paths: {}
6+
components:
7+
schemas:
8+
GeoJsonObject:
9+
type: object
10+
properties:
11+
type:
12+
type: string
13+
required:
14+
- type
15+
discriminator:
16+
propertyName: type
17+
mapping:
18+
Polygon: '#/components/schemas/Polygon'
19+
MultiPolygon: '#/components/schemas/MultiPolygon'
20+
Polygon:
21+
allOf:
22+
- $ref: '#/components/schemas/GeoJsonObject'
23+
- type: object
24+
properties:
25+
coordinates:
26+
type: array
27+
items:
28+
type: number
29+
format: double
30+
MultiPolygon:
31+
allOf:
32+
- $ref: '#/components/schemas/GeoJsonObject'
33+
- type: object
34+
properties:
35+
coordinates:
36+
type: array
37+
items:
38+
type: array
39+
items:
40+
type: number
41+
format: double

pom.xml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@
2323
<developerConnection>scm:git:git@github.com:openapitools/openapi-generator.git</developerConnection>
2424
<url>https://github.com/openapitools/openapi-generator</url>
2525
</scm>
26+
<modules>
27+
<module>modules/openapi-generator-core</module>
28+
<module>modules/openapi-generator</module>
29+
<module>modules/openapi-generator-cli</module>
30+
<module>modules/openapi-generator-maven-plugin</module>
31+
<!-- <module>modules/openapi-generator-gradle-plugin</module>-->
32+
<!-- <module>modules/openapi-generator-mill-plugin</module>-->
33+
<!-- <module>modules/openapi-generator-online</module>-->
34+
</modules>
2635
<developers>
2736
<!-- original author of the project -->
2837
<developer>
@@ -408,7 +417,6 @@
408417
<plugin>
409418
<groupId>com.gradle</groupId>
410419
<artifactId>develocity-maven-extension</artifactId>
411-
<version>${develocity-maven-extension.version}</version>
412420
<configuration>
413421
<develocity>
414422
<normalization>
@@ -1235,7 +1243,6 @@
12351243
<commons-io.version>2.20.0</commons-io.version>
12361244
<commons-lang.version>3.18.0</commons-lang.version>
12371245
<commons-text.version>1.10.0</commons-text.version>
1238-
<develocity-maven-extension.version>1.23.2</develocity-maven-extension.version>
12391246
<diffutils.version>1.3.0</diffutils.version>
12401247
<generex.version>1.0.2</generex.version>
12411248
<git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version>

0 commit comments

Comments
 (0)