Skip to content

Commit

Permalink
[codegen][validation] Add support for 'null' type (OpenAPITools#5290)
Browse files Browse the repository at this point in the history
* initial commit for null type

* Add openAPI attribute to validation and recommendation

* improve code comments. the warning used to notify the users to use  instead of defining the schema inline, but now the InlineModelResolver has been enhanced

* Add validation rule for the supported values of the 'type' attribute
  • Loading branch information
sebastien-rosset authored and MikailBag committed Mar 23, 2020
1 parent 751a4cd commit eb777e0
Show file tree
Hide file tree
Showing 12 changed files with 693 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@

import java.util.*;

/**
* Describes a single operation parameter in the OAS specification.
* A unique parameter is defined by a combination of a name and location.
* Parameters may be located in a path, query, header or cookie.
*/
public class CodegenParameter implements IJsonSchemaValidationProperties {
public boolean isFormParam, isQueryParam, isPathParam, isHeaderParam,
isCookieParam, isBodyParam, hasMore, isContainer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1761,8 +1761,8 @@ protected Schema<?> getSchemaItems(ArraySchema schema) {
}

/**
* Return the name of the allOf schema
*
* Return the name of the 'allOf' composed schema.
*
* @param names List of names
* @param composedSchema composed schema
* @return name of the allOf schema
Expand All @@ -1775,8 +1775,7 @@ public String toAllOfName(List<String> names, ComposedSchema composedSchema) {
} else if (names.size() == 1) {
return names.get(0);
} else {
LOGGER.warn("allOf with multiple schemas defined. Using only the first one: {}. " +
"To fully utilize allOf, please use $ref instead of inline schema definition", names.get(0));
LOGGER.warn("allOf with multiple schemas defined. Using only the first one: {}", names.get(0));
return names.get(0);
}
}
Expand Down Expand Up @@ -1835,13 +1834,20 @@ private String getSingleSchemaType(Schema schema) {
/**
* Return the OAI type (e.g. integer, long, etc) corresponding to a schema.
* <pre>$ref</pre> is not taken into account by this method.
*
* If the schema is free-form (i.e. 'type: object' with no properties) or inline
* schema, the returned OAI type is 'object'.
*
* @param schema
* @return type
*/
private String getPrimitiveType(Schema schema) {
if (schema == null) {
throw new RuntimeException("schema cannot be null in getPrimitiveType");
} else if (ModelUtils.isNullType(schema)) {
// The 'null' type is allowed in OAS 3.1 and above. It is not supported by OAS 3.0.x,
// though this tooling supports it.
return "null";
} else if (ModelUtils.isStringSchema(schema) && "number".equals(schema.getFormat())) {
// special handle of type: string, format: number
return "BigDecimal";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ private void flattenRequestBody(OpenAPI openAPI, String pathname, Operation oper
ObjectSchema op = (ObjectSchema) inner;
if (op.getProperties() != null && op.getProperties().size() > 0) {
flattenProperties(op.getProperties(), pathname);
// Generate a unique model name based on the title.
String modelName = resolveModelName(op.getTitle(), null);
Schema innerModel = modelFromProperty(op, modelName);
String existing = matchGenerated(innerModel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,11 @@ private String toExampleValueRecursive(Schema schema, List<String> included_sche
for (int i=0 ; i< indentation ; i++) indentation_string += " ";
String example = super.toExampleValue(schema);

if (ModelUtils.isNullType(schema) && null != example) {
// The 'null' type is allowed in OAS 3.1 and above. It is not supported by OAS 3.0.x,
// though this tooling supports it.
return "None";
}
// correct "true"s into "True"s, since super.toExampleValue uses "toString()" on Java booleans
if (ModelUtils.isBooleanSchema(schema) && null!=example) {
if ("false".equalsIgnoreCase(example)) example = "False";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -805,13 +805,25 @@ public String getSimpleTypeDeclaration(Schema schema) {
return oasType;
}

/**
* Return a string representation of the Python types for the specified schema.
* Primitive types in the OAS specification are implemented in Python using the corresponding
* Python primitive types.
* Composed types (e.g. allAll, oneOf, anyOf) are represented in Python using list of types.
*
* @param p The OAS schema.
* @param prefix prepended to the returned value.
* @param suffix appended to the returned value.
* @return a string representation of the Python types
*/
public String getTypeString(Schema p, String prefix, String suffix) {
// this is used to set dataType, which defines a python tuple of classes
String fullSuffix = suffix;
if (")".equals(suffix)) {
fullSuffix = "," + suffix;
}
if (ModelUtils.isNullable(p)) {
// Resolve $ref because ModelUtils.isXYZ methods do not automatically resolve references.
if (ModelUtils.isNullable(ModelUtils.getReferencedSchema(this.openAPI, p))) {
fullSuffix = ", none_type" + suffix;
}
if (ModelUtils.isFreeFormObject(p) && ModelUtils.getAdditionalProperties(p) == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,18 @@ private static void visitContent(OpenAPI openAPI, Content content, OpenAPISchema
}
}

/**
* Invoke the specified visitor function for every schema that matches mimeType in the OpenAPI document.
*
* To avoid infinite recursion, referenced schemas are visited only once. When a referenced schema is visited,
* it is added to visitedSchemas.
*
* @param openAPI the OpenAPI document that contains schema objects.
* @param schema the root schema object to be visited.
* @param mimeType the mime type. TODO: does not seem to be used in a meaningful way.
* @param visitedSchemas the list of referenced schemas that have been visited.
* @param visitor the visitor function which is invoked for every visited schema.
*/
private static void visitSchema(OpenAPI openAPI, Schema schema, String mimeType, List<String> visitedSchemas, OpenAPISchemaVisitor visitor) {
visitor.visit(schema, mimeType);
if (schema.get$ref() != null) {
Expand Down Expand Up @@ -648,13 +660,41 @@ public static Schema getSchema(OpenAPI openAPI, String name) {
return getSchemas(openAPI).get(name);
}

/**
* Return a Map of the schemas defined under /components/schemas in the OAS document.
* The returned Map only includes the direct children of /components/schemas in the OAS document; the Map
* does not include inlined schemas.
*
* @param openAPI the OpenAPI document.
* @return a map of schemas in the OAS document.
*/
public static Map<String, Schema> getSchemas(OpenAPI openAPI) {
if (openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) {
return openAPI.getComponents().getSchemas();
}
return Collections.emptyMap();
}

/**
* Return the list of all schemas in the 'components/schemas' section of an openAPI specification,
* including inlined schemas and children of composed schemas.
*
* @param openAPI OpenAPI document
* @return a list of schemas
*/
public static List<Schema> getAllSchemas(OpenAPI openAPI) {
List<Schema> allSchemas = new ArrayList<Schema>();
List<String> refSchemas = new ArrayList<String>();
getSchemas(openAPI).forEach((key, schema) -> {
// Invoke visitSchema to recursively visit all schema objects, included inlined and composed schemas.
// Use the OpenAPISchemaVisitor visitor function
visitSchema(openAPI, schema, null, refSchemas, (s, mimetype) -> {
allSchemas.add(s);
});
});
return allSchemas;
}

/**
* If a RequestBody contains a reference to an other RequestBody with '$ref', returns the referenced RequestBody if it is found or the actual RequestBody in the other cases.
*
Expand Down Expand Up @@ -971,7 +1011,8 @@ public static List<Schema> getInterfaces(ComposedSchema composed) {
*/
public static String getParentName(ComposedSchema composedSchema, Map<String, Schema> allSchemas) {
List<Schema> interfaces = getInterfaces(composedSchema);

int nullSchemaChildrenCount = 0;
boolean hasAmbiguousParents = false;
List<String> refedWithoutDiscriminator = new ArrayList<>();

if (interfaces != null && !interfaces.isEmpty()) {
Expand All @@ -988,19 +1029,34 @@ public static String getParentName(ComposedSchema composedSchema, Map<String, Sc
return parentName;
} else {
// not a parent since discriminator.propertyName is not set
hasAmbiguousParents = true;
refedWithoutDiscriminator.add(parentName);
}
} else {
// not a ref, doing nothing
// not a ref, doing nothing, except counting the number of times the 'null' type
// is listed as composed element.
if (ModelUtils.isNullType(schema)) {
// If there are two interfaces, and one of them is the 'null' type,
// then the parent is obvious and there is no need to warn about specifying
// a determinator.
nullSchemaChildrenCount++;
}
}
}
if (refedWithoutDiscriminator.size() == 1 && nullSchemaChildrenCount == 1) {
// One schema is a $ref and the other is the 'null' type, so the parent is obvious.
// In this particular case there is no need to specify a discriminator.
hasAmbiguousParents = false;
}
}

// parent name only makes sense when there is a single obvious parent
if (refedWithoutDiscriminator.size() == 1) {
LOGGER.warn("[deprecated] inheritance without use of 'discriminator.propertyName' is deprecated " +
"and will be removed in a future release. Generating model for composed schema name: {}. Title: {}",
composedSchema.getName(), composedSchema.getTitle());
if (hasAmbiguousParents) {
LOGGER.warn("[deprecated] inheritance without use of 'discriminator.propertyName' is deprecated " +
"and will be removed in a future release. Generating model for composed schema name: {}. Title: {}",
composedSchema.getName(), composedSchema.getTitle());
}
return refedWithoutDiscriminator.get(0);
}

Expand Down Expand Up @@ -1080,6 +1136,25 @@ else if (schema instanceof ComposedSchema) {
return false;
}

/**
* Return true if the 'nullable' attribute is set to true in the schema, i.e. if the value
* of the property can be the null value.
*
* In addition, if the OAS document is 3.1 or above, isNullable returns true if the input
* schema is a 'oneOf' composed document with at most two children, and one of the children
* is the 'null' type.
*
* The caller is responsible for resolving schema references before invoking isNullable.
* If the input schema is a $ref and the referenced schema has 'nullable: true', this method
* returns false (because the nullable attribute is defined in the referenced schema).
*
* The 'nullable' attribute was introduced in OAS 3.0.
* The 'nullable' attribute is deprecated in OAS 3.1. In a OAS 3.1 document, the preferred way
* to specify nullable properties is to use the 'null' type.
*
* @param schema the OAS schema.
* @return true if the schema is nullable.
*/
public static boolean isNullable(Schema schema) {
if (schema == null) {
return false;
Expand All @@ -1092,7 +1167,62 @@ public static boolean isNullable(Schema schema) {
if (schema.getExtensions() != null && schema.getExtensions().get("x-nullable") != null) {
return Boolean.valueOf(schema.getExtensions().get("x-nullable").toString());
}
// In OAS 3.1, the recommended way to define a nullable property or object is to use oneOf.
if (schema instanceof ComposedSchema) {
return isNullableComposedSchema(((ComposedSchema) schema));
}
return false;
}

/**
* Return true if the specified composed schema is 'oneOf', contains one or two elements,
* and at least one of the elements is the 'null' type.
*
* The 'null' type is supported in OAS 3.1 and above.
* In the example below, the 'OptionalOrder' can have the null value because the 'null'
* type is one of the elements under 'oneOf'.
*
* OptionalOrder:
* oneOf:
* - type: 'null'
* - $ref: '#/components/schemas/Order'
*
* @param schema the OAS composed schema.
* @return true if the composed schema is nullable.
*/
public static boolean isNullableComposedSchema(ComposedSchema schema) {
List<Schema> oneOf = schema.getOneOf();
if (oneOf != null && oneOf.size() <= 2) {
for (Schema s : oneOf) {
if (isNullType(s)) {
return true;
}
}
}
return false;
}

/**
* isNullType returns true if the input schema is the 'null' type.
*
* The 'null' type is supported in OAS 3.1 and above. It is not supported
* in OAS 2.0 and OAS 3.0.x.
*
* For example, the "null" type could be used to specify that a value must
* either be null or a specified type:
*
* OptionalOrder:
* oneOf:
* - type: 'null'
* - $ref: '#/components/schemas/Order'
*
* @param schema the OpenAPI schema
* @return true if the schema is the 'null' type
*/
public static boolean isNullType(Schema schema) {
if ("null".equals(schema.getType())) {
return true;
}
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ public ValidationResult validate(OpenAPI specification) {
ModelUtils.getUnusedSchemas(specification).forEach(schemaName -> validationResult.addResult(Validated.invalid(unusedSchema, "Unused model: " + schemaName)));
}

Map<String, Schema> schemas = ModelUtils.getSchemas(specification);
schemas.forEach((key, schema) -> {
// Get list of all schemas under /components/schemas, including nested schemas defined inline and composed schema.
// The validators must be able to validate every schema defined in the OAS document.
List<Schema> schemas = ModelUtils.getAllSchemas(specification);
schemas.forEach(schema -> {
SchemaWrapper wrapper = new SchemaWrapper(specification, schema);
validationResult.consume(schemaValidations.validate(wrapper));
});
Expand Down
Loading

0 comments on commit eb777e0

Please sign in to comment.