Skip to content

Commit d1cde7f

Browse files
authored
Add 2 rules to OpenAPI Normalizer (#14463)
* add REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIIES_ONLY * add rules to simplify anyOf * fix rules, update docs * remove test * fix doc
1 parent c912bae commit d1cde7f

File tree

6 files changed

+310
-26
lines changed

6 files changed

+310
-26
lines changed

docs/customization.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,11 +456,24 @@ Note: Only arrayItemSuffix, mapItemSuffix are supported at the moment. `SKIP_SCH
456456
457457
OpenAPI Normalizer (off by default) transforms the input OpenAPI doc/spec (which may not perfectly conform to the specification) to make it workable with OpenAPI Generator. Here is a list of rules supported:
458458
459-
- `REF_AS_PARENT_IN_ALLOF`: when set to `true`, child schemas in `allOf` is considered a parent if it's a `$ref` (instead of inline schema)
459+
- `REF_AS_PARENT_IN_ALLOF`: when set to `true`, child schemas in `allOf` is considered a parent if it's a `$ref` (instead of inline schema).
460460
461461
462462
Example:
463463
```
464-
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/allOf_extension_parent.yaml -o /tmp/java-okhttp/ --additional-properties hideGenerationTimestamp="true" --openapi-normalizer REF_AS_PARENT_IN_ALLOF=true
464+
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/allOf_extension_parent.yaml -o /tmp/java-okhttp/ --openapi-normalizer REF_AS_PARENT_IN_ALLOF=true
465465
```
466466
467+
- `REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY`: when set to `true`, oneOf/anyOf schema with only required properies only in a schema with properties will be removed. [(example)](modules/openapi-generator/src/test/resources/3_0/removeAnyOfOneOfAndKeepPropertiesOnly_test.yaml)
468+
469+
Example:
470+
```
471+
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/removeAnyOfOneOfAndKeepPropertiesOnly_test.yaml -o /tmp/java-okhttp/ --openapi-normalizer REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY=true
472+
```
473+
474+
- `SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING`: when set to `true`, simplify anyOf schema with string and enum of string to just `string`
475+
476+
Example:
477+
```
478+
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/simplifyAnyOfStringAndEnumString_test.yaml -o /tmp/java-okhttp/ --openapi-normalizer SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING=true
479+
```

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

Lines changed: 134 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ public class OpenAPINormalizer {
4848
final String REF_AS_PARENT_IN_ALLOF = "REF_AS_PARENT_IN_ALLOF";
4949
boolean enableRefAsParentInAllOf;
5050

51+
// when set to true, complex composed schemas (a mix of oneOf/anyOf/anyOf and properties) with
52+
// oneOf/anyOf containing only `required` and no properties (these are properties inter-dependency rules)
53+
// are removed as most generators cannot handle such case at the moment
54+
final String REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY = "REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY";
55+
boolean removeAnyOfOneOfAndKeepPropertiesOnly;
56+
57+
// when set to true, oneOf/anyOf with either string or enum string as sub schemas will be simplified
58+
// to just string
59+
final String SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING = "SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING";
60+
boolean simplifyAnyOfStringAndEnumString;
61+
5162
// ============= end of rules =============
5263

5364
/**
@@ -79,6 +90,14 @@ public void parseRules(Map<String, String> rules) {
7990
if (enableAll || "true".equalsIgnoreCase(rules.get(REF_AS_PARENT_IN_ALLOF))) {
8091
enableRefAsParentInAllOf = true;
8192
}
93+
94+
if (enableAll || "true".equalsIgnoreCase(rules.get(REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY))) {
95+
removeAnyOfOneOfAndKeepPropertiesOnly = true;
96+
}
97+
98+
if (enableAll || "true".equalsIgnoreCase(rules.get(SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING))) {
99+
simplifyAnyOfStringAndEnumString = true;
100+
}
82101
}
83102

84103
/**
@@ -235,7 +254,8 @@ private void normalizeComponents() {
235254
if (schema == null) {
236255
LOGGER.warn("{} not fount found in openapi/components/schemas.", schemaName);
237256
} else {
238-
normalizeSchema(schema, new HashSet<>());
257+
Schema result = normalizeSchema(schema, new HashSet<>());
258+
schemas.put(schemaName, result);
239259
}
240260
}
241261
}
@@ -245,19 +265,20 @@ private void normalizeComponents() {
245265
*
246266
* @param schema Schema
247267
* @param visitedSchemas a set of visited schemas
268+
* @return Schema
248269
*/
249-
public void normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
270+
public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
250271
if (schema == null) {
251-
return;
272+
return schema;
252273
}
253274

254275
if (StringUtils.isNotEmpty(schema.get$ref())) {
255276
// not need to process $ref
256-
return;
277+
return schema;
257278
}
258279

259280
if ((visitedSchemas.contains(schema))) {
260-
return; // skip due to circular reference
281+
return schema; // skip due to circular reference
261282
} else {
262283
visitedSchemas.add(schema);
263284
}
@@ -267,38 +288,47 @@ public void normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
267288
} else if (schema.getAdditionalProperties() instanceof Schema) { // map
268289
normalizeSchema((Schema) schema.getAdditionalProperties(), visitedSchemas);
269290
} else if (ModelUtils.isComposedSchema(schema)) {
270-
ComposedSchema m = (ComposedSchema) schema;
271-
if (m.getAllOf() != null && !m.getAllOf().isEmpty()) {
272-
normalizeAllOf(m, visitedSchemas);
291+
ComposedSchema cs = (ComposedSchema) schema;
292+
293+
if (ModelUtils.isComplexComposedSchema(cs)) {
294+
cs = (ComposedSchema) normalizeComplexComposedSchema(cs, visitedSchemas);
295+
}
296+
297+
if (cs.getAllOf() != null && !cs.getAllOf().isEmpty()) {
298+
return normalizeAllOf(cs, visitedSchemas);
273299
}
274300

275-
if (m.getOneOf() != null && !m.getOneOf().isEmpty()) {
276-
normalizeOneOf(m, visitedSchemas);
301+
if (cs.getOneOf() != null && !cs.getOneOf().isEmpty()) {
302+
return normalizeOneOf(cs, visitedSchemas);
277303
}
278304

279-
if (m.getAnyOf() != null && !m.getAnyOf().isEmpty()) {
280-
normalizeAnyOf(m, visitedSchemas);
305+
if (cs.getAnyOf() != null && !cs.getAnyOf().isEmpty()) {
306+
return normalizeAnyOf(cs, visitedSchemas);
281307
}
282308

283-
if (m.getProperties() != null && !m.getProperties().isEmpty()) {
284-
normalizeProperties(m.getProperties(), visitedSchemas);
309+
if (cs.getProperties() != null && !cs.getProperties().isEmpty()) {
310+
normalizeProperties(cs.getProperties(), visitedSchemas);
285311
}
286312

287-
if (m.getAdditionalProperties() != null) {
313+
if (cs.getAdditionalProperties() != null) {
288314
// normalizeAdditionalProperties(m);
289315
}
316+
317+
return cs;
290318
} else if (schema.getNot() != null) {// not schema
291319
normalizeSchema(schema.getNot(), visitedSchemas);
292320
} else if (schema.getProperties() != null && !schema.getProperties().isEmpty()) {
293321
normalizeProperties(schema.getProperties(), visitedSchemas);
294322
} else if (schema instanceof Schema) {
295-
normalizeNonComposedSchema(schema, visitedSchemas);
323+
normalizeSchemaWithOnlyProperties(schema, visitedSchemas);
296324
} else {
297325
throw new RuntimeException("Unknown schema type found in normalizer: " + schema);
298326
}
327+
328+
return schema;
299329
}
300330

301-
private void normalizeNonComposedSchema(Schema schema, Set<Schema> visitedSchemas) {
331+
private void normalizeSchemaWithOnlyProperties(Schema schema, Set<Schema> visitedSchemas) {
302332
// normalize non-composed schema (e.g. schema with only properties)
303333
}
304334

@@ -312,7 +342,7 @@ private void normalizeProperties(Map<String, Schema> properties, Set<Schema> vis
312342
}
313343
}
314344

315-
private void normalizeAllOf(Schema schema, Set<Schema> visitedSchemas) {
345+
private Schema normalizeAllOf(Schema schema, Set<Schema> visitedSchemas) {
316346
for (Object item : schema.getAllOf()) {
317347
if (!(item instanceof Schema)) {
318348
throw new RuntimeException("Error! allOf schema is not of the type Schema: " + item);
@@ -322,34 +352,55 @@ private void normalizeAllOf(Schema schema, Set<Schema> visitedSchemas) {
322352
}
323353
// process rules here
324354
processUseAllOfRefAsParent(schema);
355+
356+
return schema;
325357
}
326358

327-
private void normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
328-
for (Object item : schema.getAllOf()) {
359+
private Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
360+
for (Object item : schema.getOneOf()) {
329361
if (!(item instanceof Schema)) {
330362
throw new RuntimeException("Error! allOf schema is not of the type Schema: " + item);
331363
}
332364
// normalize oenOf sub schemas one by one
333365
normalizeSchema((Schema) item, visitedSchemas);
334366
}
367+
335368
// process rules here
369+
return schema;
336370
}
337371

338-
private void normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) {
339-
for (Object item : schema.getAllOf()) {
372+
private Schema normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) {
373+
for (Object item : schema.getAnyOf()) {
340374
if (!(item instanceof Schema)) {
341375
throw new RuntimeException("Error! allOf schema is not of the type Schema: " + item);
342376
}
343377
// normalize anyOf sub schemas one by one
344378
normalizeSchema((Schema) item, visitedSchemas);
345379
}
380+
346381
// process rules here
382+
383+
// last rule to process as the schema may become String schema (not "anyOf") after the completion
384+
return processSimplifyAnyOfStringAndEnumString(schema);
385+
}
386+
387+
private Schema normalizeComplexComposedSchema(Schema schema, Set<Schema> visitedSchemas) {
388+
389+
processRemoveAnyOfOneOfAndKeepPropertiesOnly(schema);
390+
391+
return schema;
347392
}
348393

349394
// ===================== a list of rules =====================
350395
// all rules (fuctions) start with the word "process"
396+
397+
/**
398+
* Child schemas in `allOf` is considered a parent if it's a `$ref` (instead of inline schema).
399+
*
400+
* @param schema Schema
401+
*/
351402
private void processUseAllOfRefAsParent(Schema schema) {
352-
if (!enableRefAsParentInAllOf) {
403+
if (!enableRefAsParentInAllOf && !enableAll) {
353404
return;
354405
}
355406

@@ -380,5 +431,65 @@ private void processUseAllOfRefAsParent(Schema schema) {
380431
}
381432
}
382433
}
434+
435+
/**
436+
* If the schema contains anyOf/oneOf and properties, remove oneOf/anyOf as these serve as rules to
437+
* ensure inter-dependency between properties. It's a workaround as such validation is not supported at the moment.
438+
*
439+
* @param schema Schema
440+
*/
441+
private void processRemoveAnyOfOneOfAndKeepPropertiesOnly(Schema schema) {
442+
443+
if (!removeAnyOfOneOfAndKeepPropertiesOnly && !enableAll) {
444+
return;
445+
}
446+
447+
if (((schema.getOneOf() != null && !schema.getOneOf().isEmpty())
448+
|| (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty())) // has anyOf or oneOf
449+
&& (schema.getProperties() != null && !schema.getProperties().isEmpty()) // has properties
450+
&& schema.getAllOf() == null) { // not allOf
451+
// clear oneOf, anyOf
452+
schema.setOneOf(null);
453+
schema.setAnyOf(null);
454+
}
455+
}
456+
457+
/**
458+
* If the schema is anyOf and the sub-schemas are either string or enum of string,
459+
* then simply it to just string as many generators do not yet support anyOf.
460+
*
461+
* @param schema Schema
462+
* @return Schema
463+
*/
464+
private Schema processSimplifyAnyOfStringAndEnumString(Schema schema) {
465+
if (!simplifyAnyOfStringAndEnumString && !enableAll) {
466+
return schema;
467+
}
468+
469+
Schema s0 = null, s1 = null;
470+
if (schema.getAnyOf().size() == 2) {
471+
s0 = ModelUtils.unaliasSchema(openAPI, (Schema) schema.getAnyOf().get(0));
472+
s1 = ModelUtils.unaliasSchema(openAPI, (Schema) schema.getAnyOf().get(1));
473+
} else {
474+
return schema;
475+
}
476+
477+
s0 = ModelUtils.getReferencedSchema(openAPI, s0);
478+
s1 = ModelUtils.getReferencedSchema(openAPI, s1);
479+
480+
// find the string schema (not enum)
481+
if (s0 instanceof StringSchema && s1 instanceof StringSchema) {
482+
if (((StringSchema) s0).getEnum() != null) { // s0 is enum, s1 is string
483+
return (StringSchema) s1;
484+
} else if (((StringSchema) s1).getEnum() != null) { // s1 is enum, s0 is string
485+
return (StringSchema) s0;
486+
} else { // both are string
487+
return schema;
488+
}
489+
} else {
490+
return schema;
491+
}
492+
}
493+
383494
// ===================== end of rules =====================
384495
}

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,43 @@ public static boolean isComposedSchema(Schema schema) {
476476
return false;
477477
}
478478

479+
/**
480+
* Return true if the specified schema is composed with more than one of the following:
481+
* 'oneOf', 'anyOf' or 'allOf'.
482+
*
483+
* @param schema the OAS schema
484+
* @return true if the specified schema is a Composed schema.
485+
*/
486+
public static boolean isComplexComposedSchema(Schema schema) {
487+
if (!(schema instanceof ComposedSchema)) {
488+
return false;
489+
}
490+
491+
int count = 0;
492+
493+
if (schema.getAllOf() != null && !schema.getAllOf().isEmpty()) {
494+
count++;
495+
}
496+
497+
if (schema.getOneOf() != null && !schema.getOneOf().isEmpty()) {
498+
count++;
499+
}
500+
501+
if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) {
502+
count++;
503+
}
504+
505+
if (schema.getProperties() != null && !schema.getProperties().isEmpty()) {
506+
count++;
507+
}
508+
509+
if (count > 1) {
510+
return true;
511+
}
512+
513+
return false;
514+
}
515+
479516
/**
480517
* Return true if the specified 'schema' is an object that can be extended with additional properties.
481518
* Additional properties means a Schema should support all explicitly defined properties plus any

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4301,7 +4301,8 @@ public void testInlineEnumType() {
43014301
}
43024302

43034303
@Test
4304-
public void testOpenAPINormalizer() {
4304+
public void testOpenAPINormalizerRefAsParentInAllOf() {
4305+
// to test the rule REF_AS_PARENT_IN_ALLOF
43054306
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/allOf_extension_parent.yaml");
43064307

43074308
Schema schema = openAPI.getComponents().getSchemas().get("AnotherPerson");
@@ -4324,4 +4325,40 @@ public void testOpenAPINormalizer() {
43244325
Schema schema5 = openAPI.getComponents().getSchemas().get("Person");
43254326
assertEquals(schema5.getExtensions().get("x-parent"), "abstract");
43264327
}
4328+
4329+
@Test
4330+
public void testOpenAPINormalizerRemoveAnyOfOneOfAndKeepPropertiesOnly() {
4331+
// to test the rule REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIIES_ONLY
4332+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/removeAnyOfOneOfAndKeepPropertiesOnly_test.yaml");
4333+
4334+
Schema schema = openAPI.getComponents().getSchemas().get("Person");
4335+
assertEquals(schema.getAnyOf().size(), 2);
4336+
4337+
Map<String, String> options = new HashMap<>();
4338+
options.put("REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY", "true");
4339+
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options);
4340+
openAPINormalizer.normalize();
4341+
4342+
Schema schema3 = openAPI.getComponents().getSchemas().get("Person");
4343+
assertNull(schema.getAnyOf());
4344+
}
4345+
4346+
@Test
4347+
public void testOpenAPINormalizerSimplifyOneOfAnyOfStringAndEnumString() {
4348+
// to test the rule SIMPLIFY_ONEOF_ANYOF_STRING_AND_ENUM_STRING
4349+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/simplifyAnyOfStringAndEnumString_test.yaml");
4350+
4351+
Schema schema = openAPI.getComponents().getSchemas().get("AnyOfTest");
4352+
assertEquals(schema.getAnyOf().size(), 2);
4353+
4354+
Map<String, String> options = new HashMap<>();
4355+
options.put("SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING", "true");
4356+
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options);
4357+
openAPINormalizer.normalize();
4358+
4359+
Schema schema3 = openAPI.getComponents().getSchemas().get("AnyOfTest");
4360+
assertNull(schema3.getAnyOf());
4361+
assertTrue(schema3 instanceof StringSchema);
4362+
4363+
}
43274364
}

0 commit comments

Comments
 (0)