Skip to content

Commit 0c00505

Browse files
authored
User feedback on confusing use of oneOf (#4695)
OAS 3.x specification isn't entirely clear on oneOf support. JSON Schema defines oneOf in such a way that the Schema is only valid if it validates against exactly one of the referenced Schemas. This suggests that a Schema defined with oneOf can't include additional "dynamic" properties. OpenAPI extends on this by adding the necessary discriminator object, which allows tooling to decide the intended Schema. As tooling, openapi-generator may support loose or confusing definitions in the Specification to better support our user's use cases. In this case, we may warn that while this usage is technically valid the two target specifications are unclear about the actual constraints regarding oneOf.
1 parent 6b99aed commit 0c00505

File tree

3 files changed

+38
-10
lines changed

3 files changed

+38
-10
lines changed

modules/openapi-generator-cli/src/main/java/org/openapitools/codegen/cmd/Validate.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@
2222

2323
import io.swagger.parser.OpenAPIParser;
2424
import io.swagger.v3.oas.models.OpenAPI;
25+
import io.swagger.v3.oas.models.media.ComposedSchema;
26+
import io.swagger.v3.oas.models.media.Schema;
2527
import io.swagger.v3.parser.core.models.SwaggerParseResult;
2628
import org.openapitools.codegen.utils.ModelUtils;
2729

2830
import java.util.HashSet;
2931
import java.util.List;
32+
import java.util.Map;
3033
import java.util.Set;
3134

3235
@Command(name = "validate", description = "Validate specification")
@@ -58,6 +61,21 @@ public void run() {
5861
if (unusedModels != null) {
5962
unusedModels.forEach(name -> warnings.add("Unused model: " + name));
6063
}
64+
65+
// check for loosely defined oneOf extension requirements.
66+
// This is a recommendation because the 3.0.x spec is not clear enough on usage of oneOf.
67+
// see https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.2.1.3 and the OAS section on 'Composition and Inheritance'.
68+
Map<String, Schema> schemas = ModelUtils.getSchemas(specification);
69+
schemas.forEach((key, schema) -> {
70+
if (schema instanceof ComposedSchema) {
71+
final ComposedSchema composed = (ComposedSchema) schema;
72+
if (composed.getOneOf() != null && composed.getOneOf().size() > 0) {
73+
if (composed.getProperties() != null && composed.getProperties().size() >= 1 && composed.getProperties().get("discriminator") == null) {
74+
warnings.add("Schema (oneOf) should not contain properties: " + key);
75+
}
76+
}
77+
}
78+
});
6179
}
6280
}
6381

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1789,21 +1789,18 @@ public CodegenModel fromModel(String name, Schema schema) {
17891789
List<String> allRequired = new ArrayList<String>();
17901790

17911791
// if schema has properties outside of allOf/oneOf/anyOf also add them to m
1792-
if (schema.getProperties() != null) {
1793-
addVars(m, unaliasPropertySchema(schema.getProperties()), schema.getRequired(), null, null);
1792+
if (composed.getProperties() != null && !composed.getProperties().isEmpty()) {
1793+
if (composed.getOneOf() != null && !composed.getOneOf().isEmpty()) {
1794+
LOGGER.warn("'oneOf' is intended to include only the additional optional OAS extension discriminator object. " +
1795+
"For more details, see https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.section.9.2.1.3 and the OAS section on 'Composition and Inheritance'.");
1796+
}
1797+
addVars(m, unaliasPropertySchema(composed.getProperties()), composed.getRequired(), null, null);
17941798
}
17951799

1796-
// uncomment this when https://github.com/swagger-api/swagger-parser/issues/1252 is resolved
1797-
// if schema has additionalproperties outside of allOf/oneOf/anyOf also add it to m
1798-
// if (schema.getAdditionalProperties() != null) {
1799-
// addAdditionPropertiesToCodeGenModel(m, schema);
1800-
// }
1801-
18021800
// parent model
18031801
final String parentName = ModelUtils.getParentName(composed, allDefinitions);
18041802
final List<String> allParents = ModelUtils.getAllParentsName(composed, allDefinitions);
18051803
final Schema parent = StringUtils.isBlank(parentName) || allDefinitions == null ? null : allDefinitions.get(parentName);
1806-
final boolean hasParent = StringUtils.isNotBlank(parentName);
18071804

18081805
// TODO revise the logic below to set dicriminator, xml attributes
18091806
if (supportsInheritance || supportsMixins) {

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
import java.util.ArrayList;
3737
import java.util.Collections;
38+
import java.util.HashMap;
3839
import java.util.List;
3940
import java.util.Map;
4041
import java.util.Map.Entry;
@@ -117,7 +118,18 @@ public static List<String> getAllUsedSchemas(OpenAPI openAPI) {
117118
* @return schemas a list of unused schemas
118119
*/
119120
public static List<String> getUnusedSchemas(OpenAPI openAPI) {
120-
Map<String, List<String>> childrenMap = getChildrenMap(openAPI);
121+
final Map<String, List<String>> childrenMap;
122+
Map<String, List<String>> tmpChildrenMap;
123+
try {
124+
tmpChildrenMap = getChildrenMap(openAPI);
125+
} catch (NullPointerException npe) {
126+
// in rare cases, such as a spec document with only one top-level oneOf schema and multiple referenced schemas,
127+
// the stream used in getChildrenMap will raise an NPE. Rather than modify getChildrenMap which is used by getAllUsedSchemas,
128+
// we'll catch here as a workaround for this edge case.
129+
tmpChildrenMap = new HashMap<>();
130+
}
131+
132+
childrenMap = tmpChildrenMap;
121133
List<String> unusedSchemas = new ArrayList<String>();
122134

123135
Map<String, Schema> schemas = getSchemas(openAPI);
@@ -875,6 +887,7 @@ public static Header getHeader(OpenAPI openAPI, String name) {
875887
public static Map<String, List<String>> getChildrenMap(OpenAPI openAPI) {
876888
Map<String, Schema> allSchemas = getSchemas(openAPI);
877889

890+
// FIXME: The collect here will throw NPE if a spec document has only a single oneOf hierarchy.
878891
Map<String, List<Entry<String, Schema>>> groupedByParent = allSchemas.entrySet().stream()
879892
.filter(entry -> isComposedSchema(entry.getValue()))
880893
.collect(Collectors.groupingBy(entry -> getParentName((ComposedSchema) entry.getValue(), allSchemas)));

0 commit comments

Comments
 (0)