From eb777e05831184f064bf874da5fb703cac251236 Mon Sep 17 00:00:00 2001 From: Sebastien Rosset Date: Wed, 12 Feb 2020 15:41:18 -0800 Subject: [PATCH] [codegen][validation] Add support for 'null' type (#5290) * 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 --- .../codegen/CodegenParameter.java | 5 + .../openapitools/codegen/DefaultCodegen.java | 14 +- .../codegen/InlineModelResolver.java | 1 + .../languages/PythonClientCodegen.java | 5 + .../PythonClientExperimentalCodegen.java | 14 +- .../codegen/utils/ModelUtils.java | 140 +++++++++++++++- .../validations/oas/OpenApiEvaluator.java | 6 +- .../oas/OpenApiSchemaValidations.java | 121 +++++++++++++- .../validations/oas/RuleConfiguration.java | 81 ++++++++++ .../oas/OpenApiSchemaTypeTest.java | 43 +++++ .../3_1/null-types-with-type-array.yaml | 149 ++++++++++++++++++ .../src/test/resources/3_1/null-types.yaml | 127 +++++++++++++++ 12 files changed, 693 insertions(+), 13 deletions(-) create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/validations/oas/OpenApiSchemaTypeTest.java create mode 100644 modules/openapi-generator/src/test/resources/3_1/null-types-with-type-array.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_1/null-types.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java index 06bc5de749de..e3f3a42eb75e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java @@ -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, diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 4b212f1d5069..3a797a6a6313 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -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 @@ -1775,8 +1775,7 @@ public String toAllOfName(List 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); } } @@ -1835,6 +1834,9 @@ private String getSingleSchemaType(Schema schema) { /** * Return the OAI type (e.g. integer, long, etc) corresponding to a schema. *
$ref
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 @@ -1842,6 +1844,10 @@ private String getSingleSchemaType(Schema schema) { 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"; diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java index e5e519d32c61..a388fa899eaf 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java @@ -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); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java index c621ebef024d..99a972c2ede8 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java @@ -705,6 +705,11 @@ private String toExampleValueRecursive(Schema schema, List 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"; diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientExperimentalCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientExperimentalCodegen.java index b229641dd441..2569ec2529f2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientExperimentalCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientExperimentalCodegen.java @@ -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) { 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 feda5fbe861d..27aa602df085 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 @@ -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 visitedSchemas, OpenAPISchemaVisitor visitor) { visitor.visit(schema, mimeType); if (schema.get$ref() != null) { @@ -648,6 +660,14 @@ 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 getSchemas(OpenAPI openAPI) { if (openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) { return openAPI.getComponents().getSchemas(); @@ -655,6 +675,26 @@ public static Map getSchemas(OpenAPI openAPI) { 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 getAllSchemas(OpenAPI openAPI) { + List allSchemas = new ArrayList(); + List refSchemas = new ArrayList(); + 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. * @@ -971,7 +1011,8 @@ public static List getInterfaces(ComposedSchema composed) { */ public static String getParentName(ComposedSchema composedSchema, Map allSchemas) { List interfaces = getInterfaces(composedSchema); - + int nullSchemaChildrenCount = 0; + boolean hasAmbiguousParents = false; List refedWithoutDiscriminator = new ArrayList<>(); if (interfaces != null && !interfaces.isEmpty()) { @@ -988,19 +1029,34 @@ public static String getParentName(ComposedSchema composedSchema, Map 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; } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OpenApiEvaluator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OpenApiEvaluator.java index 29e4e9dcf730..5cbe4c9eb894 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OpenApiEvaluator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OpenApiEvaluator.java @@ -49,8 +49,10 @@ public ValidationResult validate(OpenAPI specification) { ModelUtils.getUnusedSchemas(specification).forEach(schemaName -> validationResult.addResult(Validated.invalid(unusedSchema, "Unused model: " + schemaName))); } - Map 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 schemas = ModelUtils.getAllSchemas(specification); + schemas.forEach(schema -> { SchemaWrapper wrapper = new SchemaWrapper(specification, schema); validationResult.consume(schemaValidations.validate(wrapper)); }); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OpenApiSchemaValidations.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OpenApiSchemaValidations.java index d1f6478366fe..9251349391f3 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OpenApiSchemaValidations.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/OpenApiSchemaValidations.java @@ -4,10 +4,16 @@ import io.swagger.v3.oas.models.media.Schema; import org.openapitools.codegen.utils.ModelUtils; +import org.openapitools.codegen.utils.SemVer; import org.openapitools.codegen.validation.GenericValidator; import org.openapitools.codegen.validation.ValidationRule; import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.HashSet; +import java.util.Arrays; /** * A standalone instance for evaluating rules and recommendations related to OAS {@link Schema} @@ -23,6 +29,27 @@ class OpenApiSchemaValidations extends GenericValidator { OpenApiSchemaValidations::checkOneOfWithProperties )); } + if (ruleConfiguration.isEnableSchemaTypeRecommendation()) { + rules.add(ValidationRule.warn( + "Schema uses the 'null' type but OAS document is version 3.0.", + "The 'null' type is not supported in OpenAPI 3.0.x. It is supported in OpenAPI 3.1 and above. While our tooling supports this, it may cause issues with other tools.", + OpenApiSchemaValidations::checkNullType + )); + } + if (ruleConfiguration.isEnableNullableAttributeRecommendation()) { + rules.add(ValidationRule.warn( + "Schema uses the 'nullable' attribute.", + "The 'nullable' attribute is deprecated in OpenAPI 3.1, and may no longer be supported in future releases. Consider migrating to the 'null' type.", + OpenApiSchemaValidations::checkNullableAttribute + )); + } + if (ruleConfiguration.isEnableInvalidTypeRecommendation()) { + rules.add(ValidationRule.warn( + "Schema uses an invalid value for the 'type' attribute.", + "The 'type' attribute must be one of 'null', 'boolean', 'object', 'array', 'number', 'string', or 'integer'.", + OpenApiSchemaValidations::checkInvalidType + )); + } } } @@ -34,7 +61,7 @@ class OpenApiSchemaValidations extends GenericValidator { *

* Where the only examples of oneOf in OpenAPI Specification are used to define either/or type structures rather than validations. * Because of this ambiguity in the spec about what is non-standard about oneOf support, we'll warn as a recommendation that - * properties on the schema defining oneOf relationships may not be intentional in the OpenAPI Specification. +ne * properties on the schema defining oneOf relationships may not be intentional in the OpenAPI Specification. * * @param schemaWrapper An input schema, regardless of the type of schema * @return {@link ValidationRule.Pass} if the check succeeds, otherwise {@link ValidationRule.Fail} @@ -57,4 +84,96 @@ private static ValidationRule.Result checkOneOfWithProperties(SchemaWrapper sche } return result; } + + /** + * JSON Schema specifies type: 'null'. + *

+ * 'null' type is supported in OpenAPI Specification 3.1 and above. It is not supported in OpenAPI 3.0.x. + * Note: the validator invokes checkNullType() for every top-level schema in the OAS document. The method + * is not called for nested schemas that are defined inline. + * + * @param schema An input schema, regardless of the type of schema. + * @return {@link ValidationRule.Pass} if the check succeeds, otherwise {@link ValidationRule.Fail} + */ + private static ValidationRule.Result checkNullType(SchemaWrapper schemaWrapper) { + Schema schema = schemaWrapper.getSchema(); + ValidationRule.Result result = ValidationRule.Pass.empty(); + if (schemaWrapper.getOpenAPI() != null) { + SemVer version = new SemVer(schemaWrapper.getOpenAPI().getOpenapi()); + if (version.atLeast("3.0") && version.compareTo(new SemVer("3.1")) < 0) { + // OAS spec is 3.0.x + if (ModelUtils.isNullType(schema)) { + result = new ValidationRule.Fail(); + String name = schema.getName(); + if (name == null) { + name = schema.getTitle(); + } + result.setDetails(String.format(Locale.ROOT, + "Schema '%s' uses a 'null' type, which is specified in OAS 3.1 and above, but OAS document is version %s", + name, schemaWrapper.getOpenAPI().getOpenapi())); + return result; + } + } + } + return result; + } + + /** + * JSON Schema uses the 'nullable' attribute. + *

+ * The 'nullable' attribute is supported in OpenAPI Specification 3.0.x, but it is deprecated in OpenAPI 3.1 and above. + * + * @param schema An input schema, regardless of the type of schema + * @return {@link ValidationRule.Pass} if the check succeeds, otherwise {@link ValidationRule.Fail} + */ + private static ValidationRule.Result checkNullableAttribute(SchemaWrapper schemaWrapper) { + Schema schema = schemaWrapper.getSchema(); + ValidationRule.Result result = ValidationRule.Pass.empty(); + if (schemaWrapper.getOpenAPI() != null) { + SemVer version = new SemVer(schemaWrapper.getOpenAPI().getOpenapi()); + if (version.atLeast("3.1")) { + if (ModelUtils.isNullable(schema)) { + result = new ValidationRule.Fail(); + String name = schema.getName(); + if (name == null) { + name = schema.getTitle(); + } + result.setDetails(String.format(Locale.ROOT, + "OAS document is version '%s'. Schema '%s' uses 'nullable' attribute, which has been deprecated in OAS 3.1.", + schemaWrapper.getOpenAPI().getOpenapi(), name)); + return result; + } + } + } + return result; + } + + // The set of valid OAS values for the 'type' attribute. + private static Set validTypes = new HashSet( + Arrays.asList("null", "boolean", "object", "array", "number", "string", "integer")); + + /** + * Validate the OAS document uses supported values for the 'type' attribute. + *

+ * The type must be one of the following values: null, boolean, object, array, number, string, integer. + * + * @param schema An input schema, regardless of the type of schema + * @return {@link ValidationRule.Pass} if the check succeeds, otherwise {@link ValidationRule.Fail} + */ + private static ValidationRule.Result checkInvalidType(SchemaWrapper schemaWrapper) { + Schema schema = schemaWrapper.getSchema(); + ValidationRule.Result result = ValidationRule.Pass.empty(); + if (schema.getType() != null && !validTypes.contains(schema.getType())) { + result = new ValidationRule.Fail(); + String name = schema.getName(); + if (name == null) { + name = schema.getTitle(); + } + result.setDetails(String.format(Locale.ROOT, + "Schema '%s' uses the '%s' type, which is not a valid type.", + name, schema.getType())); + return result; + } + return result; + } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/RuleConfiguration.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/RuleConfiguration.java index c8238ef590e4..0ad2213ded7d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/RuleConfiguration.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/validations/oas/RuleConfiguration.java @@ -10,6 +10,9 @@ public class RuleConfiguration { private boolean enableApacheNginxUnderscoreRecommendation = defaultedBoolean(propertyPrefix + ".apache-nginx-underscore", true); private boolean enableOneOfWithPropertiesRecommendation = defaultedBoolean(propertyPrefix + ".oneof-properties-ambiguity", true); private boolean enableUnusedSchemasRecommendation = defaultedBoolean(propertyPrefix + ".unused-schemas", true); + private boolean enableSchemaTypeRecommendation = defaultedBoolean(propertyPrefix + ".schema-type", true); + private boolean enableNullableAttributeRecommendation = defaultedBoolean(propertyPrefix + ".nullable-deprecated", true); + private boolean enableInvalidTypeRecommendation = defaultedBoolean(propertyPrefix + ".invalid-type", true); private boolean enableApiRequestUriWithBodyRecommendation = defaultedBoolean(propertyPrefix + ".anti-patterns.uri-unexpected-body", true); @@ -90,8 +93,86 @@ public void setEnableOneOfWithPropertiesRecommendation(boolean enableOneOfWithPr this.enableOneOfWithPropertiesRecommendation = enableOneOfWithPropertiesRecommendation; } + /** + * Enable or Disable the recommendation check for schemas containing type definitions, specifically + * for changes between OpenAPI 3.0.x and 3.1. + * + *

+ * For more details, see {@link RuleConfiguration#isEnableSchemaTypeRecommendation()} + * + * @param enableSchemaTypeRecommendation true to enable, false to disable + */ + public void setEnableSchemaTypeRecommendation(boolean enableSchemaTypeRecommendation) { + this.enableSchemaTypeRecommendation = enableSchemaTypeRecommendation; + } + + /** + * Gets whether the recommendation check for for schemas containing type definitions. + *

+ * In OpenAPI 3.0.x, the "type" attribute must be a string value. + * In OpenAPI 3.1, the type attribute may be: + * A string value + * The 'null' value + * An array containing primitive types and the 'null' value. + * + * @return true if enabled, false if disabled + */ + public boolean isEnableSchemaTypeRecommendation() { + return enableSchemaTypeRecommendation; + } + + /** + * Enable or Disable the recommendation check for the 'nullable' attribute. + * + *

+ * For more details, see {@link RuleConfiguration#isEnableNullableAttributeRecommendation()} + * + * @param enableNullableAttributeRecommendation true to enable, false to disable + */ + public void setEnableNullableAttributeRecommendation(boolean enableNullableAttributeRecommendation) { + this.enableNullableAttributeRecommendation = enableNullableAttributeRecommendation; + } + + /** + * Gets whether the recommendation check for for schemas containing the 'nullable' attribute. + *

+ * In OpenAPI 3.0.x, the "nullable" attribute is supported. However, because it is deprecated in 3.1 + * and above, a warning is logged to prepare for OpenAPI 3.1 recommendations. + * In OpenAPI 3.1, the 'nullable' attribute is deprecated. Instead the OpenAPI specification should + * use the 'null' type. + * + * @return true if enabled, false if disabled + */ + public boolean isEnableNullableAttributeRecommendation() { + return enableNullableAttributeRecommendation; + } + + /** + * Enable or Disable the recommendation check for the 'type' attribute. + * + *

+ * For more details, see {@link RuleConfiguration#isEnableInvalidTypeRecommendation()} + * + * @param enableInvalidTypeRecommendation true to enable, false to disable + */ + public void setEnableInvalidTypeRecommendation(boolean enableInvalidTypeRecommendation) { + this.enableInvalidTypeRecommendation = enableInvalidTypeRecommendation; + } + + /** + * Gets whether the recommendation check for for schemas containing invalid values for the 'type' attribute. + *

+ * + * @return true if enabled, false if disabled + */ + public boolean isEnableInvalidTypeRecommendation() { + return enableInvalidTypeRecommendation; + } + /** * Get whether recommendations are enabled. + *

+ * The 'type' attribute must be one of 'null', 'boolean', 'object', 'array', 'number', 'string', or 'integer' * * @return true if enabled, false if disabled */ diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/validations/oas/OpenApiSchemaTypeTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/validations/oas/OpenApiSchemaTypeTest.java new file mode 100644 index 000000000000..ac8e207fb62e --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/validations/oas/OpenApiSchemaTypeTest.java @@ -0,0 +1,43 @@ +package org.openapitools.codegen.validations.oas; + +import io.swagger.v3.oas.models.media.*; +import io.swagger.v3.oas.models.OpenAPI; +import org.openapitools.codegen.TestUtils; +import org.openapitools.codegen.validation.Invalid; +import org.openapitools.codegen.validation.ValidationResult; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class OpenApiSchemaTypeTest { + @Test(dataProvider = "oas31RecommendationExpectations", description = "Warn when 3.1 features are present in a OAS 3.0 document") + public void testOas30DocumentWithNullType(final OpenAPI openAPI, boolean matches) { + RuleConfiguration config = new RuleConfiguration(); + config.setEnableRecommendations(true); + OpenApiEvaluator validator = new OpenApiEvaluator(config); + ValidationResult result = validator.validate(openAPI); + Assert.assertNotNull(result.getWarnings()); + + List warnings = result.getWarnings().stream() + .filter(invalid -> "Schema uses the 'null' type but OAS document is version 3.0." .equals(invalid.getRule().getDescription())) + .collect(Collectors.toList()); + + Assert.assertNotNull(warnings); + if (matches) { + Assert.assertEquals(warnings.size() >= 1, true, "Expected to match recommendation."); + } else { + Assert.assertEquals(warnings.size(), 0, "Expected not to match recommendation."); + } + } + + @DataProvider(name = "oas31RecommendationExpectations") + public Object[][] oas31RecommendationExpectations() { + return new Object[][]{ + {TestUtils.parseSpec("src/test/resources/3_1/null-types.yaml"), true} + }; + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_1/null-types-with-type-array.yaml b/modules/openapi-generator/src/test/resources/3_1/null-types-with-type-array.yaml new file mode 100644 index 000000000000..b01e611e37ad --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/null-types-with-type-array.yaml @@ -0,0 +1,149 @@ +# OAS document that uses 3.1 features: +# 'null' type +# type array +openapi: 3.1.0 +info: + version: 1.0.0 + title: Example + license: + name: MIT +servers: + - url: http://api.example.xyz/v1 +paths: + /person/display/{personId}: + get: + parameters: + - name: personId + in: path + required: true + description: The id of the person to retrieve + schema: + type: string + operationId: list + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Person" +components: + schemas: + # The purpose of this OAS document is to test the OpenAPITool validator and generator against + # valid OAS syntax. It is not written to achieve good model design. + Person: + type: object + discriminator: + propertyName: $_type + mapping: + a: '#/components/schemas/Adult' + c: Child + properties: + $_type: + type: string + lastName: + type: string + firstName: + type: string + maidenName: + # A property which can be the 'null' value or a string value. + # This allows for a concise expression of use cases such as a function that might + # return either a string of a certain length or a null value: + # The 'maxLength' assertion only constrain values within a certain primitive + # type. When the type of the instance is not of the type targeted by + # the keyword, the instance is considered to conform to the assertion. + # See https://tools.ietf.org/html/draft-handrews-json-schema-02#section-7.6.1 + type: [ string, 'null'] + maxLength: 255 + address: + # An object defined inline instead of by reference. codegen and language generators + # behave differently when a property is defined inline versus by reference. + type: object + properties: + street: + type: string + city: + type: string + secondHomeAddress: + # The value of this property may be the null value because the 'Nullable.Address' + # schema has set the 'nullable' attribute to true. + $ref: "#/components/schemas/Nullable.Address" + skills: + # An array defined inline instead of by reference. codegen and language generators + # behave differently when a property is defined inline versus by reference. + type: array + items: + type: object + properties: + name: + type: string + nullableSkills: + # An array defined inline instead of by reference. codegen and language generators + # behave differently when a property is defined inline versus by reference. + # The value can be 'null'. + type: [array, 'null'] + items: + type: object + properties: + name: + type: string + nullableSkillsVariant2: + # An array defined inline instead of by reference. codegen and language generators + # behave differently when a property is defined inline versus by reference. + # The value can be 'null'. + oneOf: + - type: 'null' + - type: array + items: + type: object + properties: + name: + type: string + Adult: + description: A representation of an adult + allOf: + - $ref: '#/components/schemas/Person' + - type: object + properties: + children: + # The value of this property may not be null. + # Some language generators are lenient by default (either intentionally or by accident) + # and would allow the 'null' value, but from a compliance perspective, the null value + # is not allowed. + type: array + items: + $ref: "#/components/schemas/Child" + nullableChildren: + # Use 'type array' syntax to indicate the value of the property may be the 'null' value or an array. + # Type arrays are supported in OAS 3.1 and above. + type: [array, 'null'] + items: + $ref: "#/components/schemas/Child" + nullableChildrenVariant2: + # Use 'oneOf' syntax to indicate the value of the property may be the 'null' value or an array. + # The 'null' type is supported in OAS 3.1 and above. + oneOf: + - type: 'null' + - type: array + items: + $ref: "#/components/schemas/Child" + Child: + description: A representation of a child + allOf: + - type: object + properties: + age: + type: integer + format: int32 + - $ref: '#/components/schemas/Person' + Nullable.Address: + description: A representation of an address whose value can be null. + type: object + # Note the 'nullable' attribute has been deprecated in OAS 3.1, hence it should be + # avoided in favor of using the 'null' type. + nullable: true + properties: + street: + type: string + city: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_1/null-types.yaml b/modules/openapi-generator/src/test/resources/3_1/null-types.yaml new file mode 100644 index 000000000000..1d7df45ab540 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/null-types.yaml @@ -0,0 +1,127 @@ +# OAS document that uses the 'null' type, which is a 3.1 feature. +openapi: 3.0.2 # This should be 3.1, but the parser does not accept that version yet. +info: + version: 1.0.0 + title: Example + license: + name: MIT +servers: + - url: http://api.example.xyz/v1 +paths: + /person/display/{personId}: + get: + parameters: + - name: personId + in: path + required: true + description: The id of the person to retrieve + schema: + type: string + operationId: list + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Person" +components: + schemas: + # The purpose of this OAS document is to test the OpenAPITool validator and generator against + # valid OAS syntax. It is not written to achieve good model design. + Person: + type: object + discriminator: + propertyName: $_type + mapping: + a: '#/components/schemas/Adult' + c: Child + properties: + $_type: + type: string + lastName: + type: string + firstName: + type: string + maidenName: + # A property which can be the 'null' value or a string value. + oneOf: + - type: string + maxLength: 255 + - type: 'null' + address: + # An object defined inline instead of by reference. codegen and language generators + # behave differently when a property is defined inline versus by reference. + type: object + properties: + street: + type: string + city: + type: string + secondHomeAddress: + # The value of this property may be the null value because the 'Nullable.Address' + # schema has set the 'nullable' attribute to true. + $ref: "#/components/schemas/Nullable.Address" + skills: + # An array defined inline instead of by reference. codegen and language generators + # behave differently when a property is defined inline versus by reference. + type: array + items: + type: object + properties: + name: + type: string + nullableSkillsVariant2: + # An array defined inline instead of by reference. codegen and language generators + # behave differently when a property is defined inline versus by reference. + # The value can be 'null'. + oneOf: + - type: 'null' + - type: array + items: + type: object + properties: + name: + type: string + Adult: + description: A representation of an adult + allOf: + - $ref: '#/components/schemas/Person' + - type: object + properties: + children: + # The value of this property may not be null. + # Some language generators are lenient by default (either intentionally or by accident) + # and would allow the 'null' value, but from a compliance perspective, the null value + # is not allowed. + type: array + items: + $ref: "#/components/schemas/Child" + nullableChildrenVariant2: + # Use 'oneOf' syntax to indicate the value of the property may be the 'null' value or an array. + # The 'null' type is supported in OAS 3.1 and above. + oneOf: + - type: 'null' + - type: array + items: + $ref: "#/components/schemas/Child" + Child: + description: A representation of a child + allOf: + - type: object + properties: + age: + type: integer + format: int32 + - $ref: '#/components/schemas/Person' + Nullable.Address: + description: A representation of an address whose value can be null. + type: object + # Note the 'nullable' attribute has been deprecated in OAS 3.1, hence it should be + # avoided in favor of using the 'null' type. + nullable: true + properties: + street: + type: string + city: + type: string