From 4c5e2d3a9dc291e6ab9e994b84de98c796756e35 Mon Sep 17 00:00:00 2001 From: gracekarina Date: Mon, 11 Jul 2022 12:40:46 -0500 Subject: [PATCH 1/2] initial Swagger Parser CLI Changes form-data schema to ObjectSchema Fixes #1529 According to the OAS3 specification multipart request should be as follows: https://swagger.io/docs/specification/describing-request-body/multipart-requests/ refs #1757 - add boolean schema support and fix type inference fix: issue 1767 - OpenAPI v2 converter - security of operation should be set to empty list if that's the case in the parsed file fix and test for issue 1758 added more scenarios for refs validation adding examples scenario rephrase error message --- modules/swagger-parser-cli/pom.xml | 74 +++ .../io/swagger/v3/parser/SwaggerParser.java | 31 ++ .../src/test/java/SwaggerParserCLITest.java | 28 ++ .../resources/fileWithNoErrorMessages.yaml | 55 +++ .../fileWithValidationErrorMessages.yaml | 53 +++ .../v3/parser/core/models/ParseOptions.java | 10 +- .../v3/parser/converter/SwaggerConverter.java | 15 +- .../swagger/parser/test/V2ConverterTest.java | 43 +- .../src/test/resources/issue-1529.json | 81 ++++ .../src/test/resources/issue-1767.yaml | 41 ++ .../extensions/JsonSchemaParserExtension.java | 4 +- .../v3/parser/util/OpenAPIDeserializer.java | 449 ++++++++++-------- .../parser/test/OAI31DeserializationTest.java | 14 +- .../v3/parser/test/OpenAPIV3ParserTest.java | 27 +- .../parser/util/OpenAPIDeserializerTest.java | 223 +++++++++ .../src/test/resources/issue-1761.yaml | 214 +++++++++ .../src/test/resources/issue1758.yaml | 55 +++ pom.xml | 3 +- 18 files changed, 1176 insertions(+), 244 deletions(-) create mode 100644 modules/swagger-parser-cli/pom.xml create mode 100644 modules/swagger-parser-cli/src/main/java/io/swagger/v3/parser/SwaggerParser.java create mode 100644 modules/swagger-parser-cli/src/test/java/SwaggerParserCLITest.java create mode 100644 modules/swagger-parser-cli/src/test/resources/fileWithNoErrorMessages.yaml create mode 100644 modules/swagger-parser-cli/src/test/resources/fileWithValidationErrorMessages.yaml create mode 100644 modules/swagger-parser-v2-converter/src/test/resources/issue-1529.json create mode 100644 modules/swagger-parser-v2-converter/src/test/resources/issue-1767.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/issue-1761.yaml create mode 100644 modules/swagger-parser-v3/src/test/resources/issue1758.yaml diff --git a/modules/swagger-parser-cli/pom.xml b/modules/swagger-parser-cli/pom.xml new file mode 100644 index 0000000000..b27ac4302e --- /dev/null +++ b/modules/swagger-parser-cli/pom.xml @@ -0,0 +1,74 @@ + + + + swagger-parser-project + io.swagger.parser.v3 + 2.1.2-SNAPSHOT + + 4.0.0 + + swagger-parser-cli + jar + swagger-parser (executable) + + + swagger-parser-cli + + + src/main/resources + true + + logback.xml + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + + process-resources + package + + shade + + + false + true + + ${java.io.tmpdir}/dependency-reduced-pom.xml + + + + + + + + + + + + + + io.swagger.parser.v3 + swagger-parser-v3 + 2.1.2-SNAPSHOT + compile + + + org.testng + testng + + + + + 8 + 8 + + + \ No newline at end of file diff --git a/modules/swagger-parser-cli/src/main/java/io/swagger/v3/parser/SwaggerParser.java b/modules/swagger-parser-cli/src/main/java/io/swagger/v3/parser/SwaggerParser.java new file mode 100644 index 0000000000..042ca977d1 --- /dev/null +++ b/modules/swagger-parser-cli/src/main/java/io/swagger/v3/parser/SwaggerParser.java @@ -0,0 +1,31 @@ +package io.swagger.v3.parser; + +import io.swagger.v3.parser.core.models.SwaggerParseResult; +import java.util.ArrayList; +import java.util.List; + +public class SwaggerParser { + public static void main(String[] args) { + if (args.length > 0){ + List messages = readFromLocation(args[0]); + if ( messages.size() > 0){ + messages.forEach(System.out::println); + System.exit(1); + } + } + } + + public static List readFromLocation(String location) { + List messages = new ArrayList<>(); + try { + final SwaggerParseResult result = new OpenAPIV3Parser().readLocation(location, null, null); + if(result.getOpenAPI() == null || !result.getMessages().isEmpty()){ + messages = result.getMessages(); + } + }catch (Exception e){ + e.printStackTrace(); + System.exit(1); + } + return messages; + } +} diff --git a/modules/swagger-parser-cli/src/test/java/SwaggerParserCLITest.java b/modules/swagger-parser-cli/src/test/java/SwaggerParserCLITest.java new file mode 100644 index 0000000000..258d610fcf --- /dev/null +++ b/modules/swagger-parser-cli/src/test/java/SwaggerParserCLITest.java @@ -0,0 +1,28 @@ +import io.swagger.v3.parser.SwaggerParser; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class SwaggerParserCLITest { + @Test + public void validateOKFromLocationTest(){ + String []args = new String[1]; + args[0] = "src/test/resources/fileWithNoErrorMessages.yaml"; + Assert.assertTrue(SwaggerParser.readFromLocation(args[0]).size() == 0); + } + + @Test + public void validateErrorFromLocationTest(){ + String []args = new String[1]; + args[0] = "src/test/resources/fileWithValidationErrorMessages.yaml"; + Assert.assertEquals(SwaggerParser.readFromLocation(args[0]).get(0), "attribute info.version is missing"); + Assert.assertEquals(SwaggerParser.readFromLocation(args[0]).get(1), "attribute paths.'/cu'(post).responses.200.description is missing"); + } + + @Test + public void validateFileNotFoundInLocationTest(){ + String []args = new String[1]; + args[0] = "src/test/resources/WrongLocation.yaml"; + Assert.assertTrue(SwaggerParser.readFromLocation(args[0]).size() == 1); + Assert.assertEquals(SwaggerParser.readFromLocation(args[0]).get(0), "Unable to read location `src/test/resources/WrongLocation.yaml`"); + } +} diff --git a/modules/swagger-parser-cli/src/test/resources/fileWithNoErrorMessages.yaml b/modules/swagger-parser-cli/src/test/resources/fileWithNoErrorMessages.yaml new file mode 100644 index 0000000000..37c77fbe90 --- /dev/null +++ b/modules/swagger-parser-cli/src/test/resources/fileWithNoErrorMessages.yaml @@ -0,0 +1,55 @@ +openapi: 3.0.0 +info: + description: test + title: test + version: 1.0 +paths: + /cu: + post: + operationId: savecu + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/AbTestFoo" + "/bar": + put: + operationId: updateBar + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/CoTestBar" +servers: + - url: /foo/bar +components: + schemas: + Thing: + type: object + properties: + moreThings: + type: array + uniqueItems: true + items: + $ref: "#/components/schemas/ThingAs" + ThingAs: + type: object + properties: + concept: + $ref: "#/components/schemas/Thing" + AbTestFoo: + type: object + properties: + moreThings: + type: array + uniqueItems: true + items: + $ref: "#/components/schemas/ThingAs" + readOnly: true + CoTestBar: + allOf: + - $ref: "#/components/schemas/Thing" diff --git a/modules/swagger-parser-cli/src/test/resources/fileWithValidationErrorMessages.yaml b/modules/swagger-parser-cli/src/test/resources/fileWithValidationErrorMessages.yaml new file mode 100644 index 0000000000..a8318bbbd7 --- /dev/null +++ b/modules/swagger-parser-cli/src/test/resources/fileWithValidationErrorMessages.yaml @@ -0,0 +1,53 @@ +openapi: 3.0.0 +info: + description: test + title: test +paths: + /cu: + post: + operationId: savecu + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/AbTestFoo" + "/bar": + put: + operationId: updateBar + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/CoTestBar" +servers: + - url: /foo/bar +components: + schemas: + Thing: + type: object + properties: + moreThings: + type: array + uniqueItems: true + items: + $ref: "#/components/schemas/ThingAs" + ThingAs: + type: object + properties: + concept: + $ref: "#/components/schemas/Thing" + AbTestFoo: + type: object + properties: + moreThings: + type: array + uniqueItems: true + items: + $ref: "#/components/schemas/ThingAs" + readOnly: true + CoTestBar: + allOf: + - $ref: "#/components/schemas/Thing" diff --git a/modules/swagger-parser-core/src/main/java/io/swagger/v3/parser/core/models/ParseOptions.java b/modules/swagger-parser-core/src/main/java/io/swagger/v3/parser/core/models/ParseOptions.java index 2ed2dd6f80..a3d448431b 100644 --- a/modules/swagger-parser-core/src/main/java/io/swagger/v3/parser/core/models/ParseOptions.java +++ b/modules/swagger-parser-core/src/main/java/io/swagger/v3/parser/core/models/ParseOptions.java @@ -15,7 +15,7 @@ public class ParseOptions { private boolean resolveRequestBody = false; private boolean oaiAuthor; - private boolean defaultSchemaTypeObject = true; + private boolean inferSchemaType = true; public boolean isResolve() { return resolve; @@ -124,11 +124,11 @@ public boolean isValidateInternalRefs() { return validateInternalRefs; } - public boolean isDefaultSchemaTypeObject() { - return defaultSchemaTypeObject; + public boolean isInferSchemaType() { + return inferSchemaType; } - public void setDefaultSchemaTypeObject(boolean defaultSchemaTypeObject) { - this.defaultSchemaTypeObject = defaultSchemaTypeObject; + public void setInferSchemaType(boolean inferSchemaType) { + this.inferSchemaType = inferSchemaType; } } diff --git a/modules/swagger-parser-v2-converter/src/main/java/io/swagger/v3/parser/converter/SwaggerConverter.java b/modules/swagger-parser-v2-converter/src/main/java/io/swagger/v3/parser/converter/SwaggerConverter.java index 83f62bedf1..6ed3f26957 100644 --- a/modules/swagger-parser-v2-converter/src/main/java/io/swagger/v3/parser/converter/SwaggerConverter.java +++ b/modules/swagger-parser-v2-converter/src/main/java/io/swagger/v3/parser/converter/SwaggerConverter.java @@ -37,13 +37,7 @@ import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; -import io.swagger.v3.oas.models.media.ArraySchema; -import io.swagger.v3.oas.models.media.ComposedSchema; -import io.swagger.v3.oas.models.media.Content; -import io.swagger.v3.oas.models.media.Discriminator; -import io.swagger.v3.oas.models.media.FileSchema; -import io.swagger.v3.oas.models.media.MediaType; -import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.*; import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.responses.ApiResponse; @@ -66,7 +60,6 @@ import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -653,7 +646,7 @@ public Operation convert(io.swagger.models.Operation v2Operation) { operation.setExternalDocs(convert(v2Operation.getExternalDocs())); } - if (v2Operation.getSecurity() != null && v2Operation.getSecurity().size() > 0) { + if (v2Operation.getSecurity() != null) { operation.setSecurity(convertSecurityRequirementsMap(v2Operation.getSecurity())); } @@ -679,7 +672,7 @@ private Schema convertFormDataToSchema(io.swagger.models.parameters.Parameter fo private RequestBody convertFormDataToRequestBody(List formParams, List consumes) { RequestBody body = new RequestBody(); - Schema formSchema = new Schema(); + Schema formSchema = new ObjectSchema(); for (io.swagger.models.parameters.Parameter param : formParams) { SerializableParameter sp; @@ -702,7 +695,7 @@ private RequestBody convertFormDataToRequestBody(List mediaTypes = new ArrayList<>(globalConsumes); diff --git a/modules/swagger-parser-v2-converter/src/test/java/io/swagger/parser/test/V2ConverterTest.java b/modules/swagger-parser-v2-converter/src/test/java/io/swagger/parser/test/V2ConverterTest.java index cbfa88547a..4ff09afe4f 100644 --- a/modules/swagger-parser-v2-converter/src/test/java/io/swagger/parser/test/V2ConverterTest.java +++ b/modules/swagger-parser-v2-converter/src/test/java/io/swagger/parser/test/V2ConverterTest.java @@ -7,16 +7,12 @@ import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.headers.Header; import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.media.ArraySchema; -import io.swagger.v3.oas.models.media.BooleanSchema; -import io.swagger.v3.oas.models.media.ComposedSchema; -import io.swagger.v3.oas.models.media.IntegerSchema; -import io.swagger.v3.oas.models.media.StringSchema; -import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.*; import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.security.OAuthFlow; +import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.tags.Tag; import io.swagger.v3.parser.converter.SwaggerConverter; @@ -99,6 +95,8 @@ public class V2ConverterTest { private static final String ISSUE_1715_YAML = "issue-1715.yaml"; + private static final String ISSUE_1767_YAML = "issue-1767.yaml"; + private static final String API_BATCH_PATH = "/api/batch/"; private static final String PETS_PATH = "/pets"; private static final String PET_FIND_BY_STATUS_PATH = "/pet/findByStatus"; @@ -864,6 +862,21 @@ public void testissue1715() throws Exception { assertEquals("bar", requestBody.getExtensions().get("x-foo")); } + @Test(description = "OpenAPI v2 converter - security of operation should be set to empty list if that's the case") + public void testIssue1767() throws Exception { + OpenAPI oas = getConvertedOpenAPIFromJsonFile(ISSUE_1767_YAML); + assertNotNull(oas); + + List firstOperationSecurityRequirements = + oas.getPaths().get("/api/not-secured").getGet().getSecurity(); + assertNotNull(firstOperationSecurityRequirements); + assertEquals(firstOperationSecurityRequirements.size(), 0); + + List secondOperationSecurityRequirements = + oas.getPaths().get("/api/secured/").getGet().getSecurity(); + assertNull(secondOperationSecurityRequirements); + } + @Test() public void testInlineDefinitionProperty() throws Exception { SwaggerConverter converter = new SwaggerConverter(); @@ -882,4 +895,22 @@ public void testInlineDefinitionProperty() throws Exception { assertNotNull(petCategoryInline); } + + @Test() + public void testConvertFormDataAsObjectSchema() throws Exception { + OpenAPI oas = getConvertedOpenAPIFromJsonFile("issue-1529.json"); + assertNotNull(oas); + + Schema companiesSchema = oas.getPaths() + .get("/companies/") + .getPost() + .getRequestBody() + .getContent() + .get("multipart/form-data") + .getSchema(); + + assertEquals(companiesSchema.getType(), "object"); + assertEquals(companiesSchema.getClass(), ObjectSchema.class); + + } } diff --git a/modules/swagger-parser-v2-converter/src/test/resources/issue-1529.json b/modules/swagger-parser-v2-converter/src/test/resources/issue-1529.json new file mode 100644 index 0000000000..df2f7bc83a --- /dev/null +++ b/modules/swagger-parser-v2-converter/src/test/resources/issue-1529.json @@ -0,0 +1,81 @@ +{ + "basePath": "/external-api/v3", + "consumes": [ + "application/json" + ], + + "host": "host.systems", + "info": { + "description": "test form data", + "title": "test form data", + "version": "v0" + }, + "paths": { + "/companies/": { + "post": { + "consumes": [ + "multipart/form-data" + ], + "description": "Create company.", + "operationId": "companies_create", + "parameters": [ + { + "description": "Accessible extensions are: `.jpg`, `.png`, maximum size is: `5.0` Mb", + "in": "formData", + "name": "profile_image", + "required": true, + "type": "file" + }, + { + "format": "date", + "in": "formData", + "name": "expiry", + "required": false, + "type": "string", + "x-nullable": true + }, + { + "in": "formData", + "maxLength": 250, + "minLength": 1, + "name": "name", + "required": true, + "type": "string" + }, + { + "description": "Accessible extensions are: `.svg`, `.png`, maximum size is: `5.0` Mb", + "in": "formData", + "name": "logo", + "required": false, + "type": "file", + "x-nullable": true + } + ], + "responses": { + "201": { + "description": "response", + "schema": { + "type": "object", + "properties": { + "status" : { + "type": "string" + } + } + } + } + }, + "summary": "Create company.", + "tags": [ + "companies" + ] + } + } + }, + "produces": [ + "application/json" + ], + "schemes": [ + "http" + ], + "swagger": "2.0" +} \ No newline at end of file diff --git a/modules/swagger-parser-v2-converter/src/test/resources/issue-1767.yaml b/modules/swagger-parser-v2-converter/src/test/resources/issue-1767.yaml new file mode 100644 index 0000000000..37195e0c33 --- /dev/null +++ b/modules/swagger-parser-v2-converter/src/test/resources/issue-1767.yaml @@ -0,0 +1,41 @@ +swagger: '2.0' +basePath: / +paths: + /api/not-secured: + get: + responses: + '200': + description: Success + summary: Not secured API + operationId: not_secured_api + security: [] + tags: + - not-secured + /api/secured/: + get: + responses: + '200': + description: Success + summary: Secured API + operationId: secured_api + tags: + - secured +info: + title: Sample spec + version: 0.1.0 +produces: + - application/json +consumes: + - application/json +securityDefinitions: + api_key: + type: apiKey + in: header + name: Authorization +security: + - api_key: [] +tags: + - name: not-secured + description: API not secured + - name: secured + description: API secured diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/extensions/JsonSchemaParserExtension.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/extensions/JsonSchemaParserExtension.java index eeab9768ea..54b58da372 100644 --- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/extensions/JsonSchemaParserExtension.java +++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/extensions/JsonSchemaParserExtension.java @@ -1,6 +1,6 @@ package io.swagger.v3.parser.extensions; -import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.parser.ResolverCache; @@ -10,7 +10,7 @@ public interface JsonSchemaParserExtension { - Schema getSchema(ObjectNode node, String location, OpenAPIDeserializer.ParseResult result, Map rootMap, String basePath); + Schema getSchema(JsonNode node, String location, OpenAPIDeserializer.ParseResult result, Map rootMap, String basePath); boolean resolveSchema(Schema schema, ResolverCache cache, OpenAPI openAPI, boolean openapi31); diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/OpenAPIDeserializer.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/OpenAPIDeserializer.java index 95d3d20e41..7c80f9108b 100644 --- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/OpenAPIDeserializer.java +++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/OpenAPIDeserializer.java @@ -286,7 +286,7 @@ public SwaggerParseResult deserialize(JsonNode rootNode, String path, ParseOptio try { ParseResult rootParse = new ParseResult(); rootParse.setOaiAuthor(options.isOaiAuthor()); - rootParse.setDefaultSchemaTypeObject(options.isDefaultSchemaTypeObject()); + rootParse.setInferSchemaType(options.isInferSchemaType()); rootParse.setAllowEmptyStrings(options.isAllowEmptyString()); rootParse.setValidateInternalRefs(options.isValidateInternalRefs()); OpenAPI api = parseRoot(rootNode, rootParse, path); @@ -958,6 +958,9 @@ public PathItem getPathItem(ObjectNode obj, String location, ParseResult result) } else { pathItem.set$ref(ref.textValue()); } + if(ref.textValue().startsWith("#/components") && !(ref.textValue().startsWith("#/components/pathItems"))) { + result.warning(location, "$ref target "+ref.textValue() +" is not of expected type PathItem"); + } if(result.isOpenapi31()){ String value = getString("summary", obj, false, location, result); if (StringUtils.isNotBlank(value)) { @@ -1176,6 +1179,39 @@ public ObjectNode getObject(String key, ObjectNode node, boolean required, Strin return object; } + public JsonNode getObjectOrBoolean(String key, ObjectNode node, boolean required, String location, ParseResult result) { + JsonNode value = node.get(key); + + if (value == null) { + if (required) { + result.missing(location, key); + result.invalid(); + } + return null; + } + Boolean boolValue = null; + if (value.getNodeType().equals(JsonNodeType.BOOLEAN)) { + boolValue = value.asBoolean(); + } else if (value.getNodeType().equals(JsonNodeType.STRING)) { + String stringValue = value.textValue(); + if ("true".equalsIgnoreCase(stringValue) || "false".equalsIgnoreCase(stringValue)) { + boolValue = Boolean.parseBoolean(stringValue); + } else { + result.invalidType(location, key, "object", value); + return null; + } + } + if (boolValue != null) { + return value; + } + if (!value.isObject()) { + result.invalidType(location, key, "object", value); + return null; + } + + return value; + } + public Info getInfo(ObjectNode node, String location, ParseResult result) { if (node == null) return null; @@ -1535,6 +1571,9 @@ public Link getLink(ObjectNode linkNode, String location, ParseResult result) { } else { link.set$ref(ref.textValue()); } + if(ref.textValue().startsWith("#/components") && !(ref.textValue().startsWith("#/components/links"))) { + result.warning(location, "$ref target "+ref.textValue() +" is not of expected type Link"); + } if (result.isOpenapi31()) { String desc = getString("description", linkNode, false, location, result); if (StringUtils.isNotBlank(desc)) { @@ -1659,6 +1698,9 @@ public Callback getCallback(ObjectNode node, String location, ParseResult result } else { callback.set$ref(ref.textValue()); } + if(ref.textValue().startsWith("#/components") && !(ref.textValue().startsWith("#/components/callbacks"))) { + result.warning(location, "$ref target "+ref.textValue() +" is not of expected type Callback"); + } return callback; } else { result.invalidType(location, "$ref", "string", node); @@ -1905,10 +1947,13 @@ public Parameter getParameter(ObjectNode obj, String location, ParseResult resul parameter = new Parameter(); String mungedRef = mungedRef(ref.textValue()); if (mungedRef != null) { - parameter.set$ref(mungedRef); - } else { - parameter.set$ref(ref.textValue()); - } + parameter.set$ref(mungedRef); + }else { + parameter.set$ref(ref.textValue()); + } + if(ref.textValue().startsWith("#/components") && !(ref.textValue().startsWith("#/components/parameters") || ref.textValue().startsWith("#/components/headers"))) { + result.warning(location, "$ref target "+ref.textValue() +" is not of expected type Parameter/Header"); + } if (result.isOpenapi31()) { String desc = getString("description", obj, false, location, result); if (StringUtils.isNotBlank(desc)) { @@ -2115,6 +2160,10 @@ public Header getHeader(ObjectNode headerNode, String location, ParseResult resu } else { header.set$ref(ref.textValue()); } + if(ref.textValue().startsWith("#/components") && !(ref.textValue().startsWith("#/components/parameters") || ref.textValue().startsWith("#/components/headers"))) { + result.warning(location, "$ref target "+ref.textValue() +" is not of expected type Header/Parameter"); + } + if (result.isOpenapi31()) { String desc = getString("description", headerNode, false, location, result); if (StringUtils.isNotBlank(desc)) { @@ -2277,6 +2326,9 @@ public SecurityScheme getSecurityScheme(ObjectNode node, String location, ParseR } else { securityScheme.set$ref(ref.textValue()); } + if(ref.textValue().startsWith("#/components") && !(ref.textValue().startsWith("#/components/securitySchemes"))) { + result.warning(location, "$ref target "+ref.textValue() +" is not of expected type securitySchemes"); + } if (result.isOpenapi31()) { String desc = getString("description", node, false, location, result); if (StringUtils.isNotBlank(desc)) { @@ -2545,8 +2597,8 @@ public Discriminator getDiscriminator(ObjectNode node, String location, ParseRes } - public Schema getSchema(ObjectNode node, String location, ParseResult result) { - if (node == null) { + public Schema getSchema(JsonNode jsonNode, String location, ParseResult result) { + if (jsonNode == null) { return null; } //Added to handle NPE from ResolverCache when Trying to dereference a schema @@ -2564,16 +2616,23 @@ at the moment path passed as string (basePath) from upper components can be both */ for (JsonSchemaParserExtension jsonschemaExtension: jsonschemaExtensions) { - schema = jsonschemaExtension.getSchema(node, location, result, rootMap, basePath); + schema = jsonschemaExtension.getSchema(jsonNode, location, result, rootMap, basePath); if (schema != null) { return schema; } } if (result.isOpenapi31()) { - return getJsonSchema(node, location, result); + return getJsonSchema(jsonNode, location, result); } + ObjectNode node = null; + if (jsonNode.isObject()) { + node = (ObjectNode) jsonNode; + } else { + result.invalidType(location, "", "object", jsonNode); + return null; + } ArrayNode oneOfArray = getArray("oneOf", node, false, location, result); ArrayNode allOfArray = getArray("allOf", node, false, location, result); ArrayNode anyOfArray = getArray("anyOf", node, false, location, result); @@ -2614,7 +2673,7 @@ at the moment path passed as string (basePath) from upper components can be both } } - if (itemsNode != null && result.isDefaultSchemaTypeObject()) { + if (itemsNode != null && result.isInferSchemaType()) { ArraySchema items = new ArraySchema(); if (itemsNode.getNodeType().equals(JsonNodeType.OBJECT)) { items.setItems(getSchema(itemsNode, location, result)); @@ -2652,7 +2711,7 @@ at the moment path passed as string (basePath) from upper components can be both ? getSchema(additionalPropertiesObject, location, result) : additionalPropertiesBoolean; - if (additionalProperties != null && result.isDefaultSchemaTypeObject()) { + if (additionalProperties != null && result.isInferSchemaType()) { if (schema == null) { schema = additionalProperties.equals(Boolean.FALSE) @@ -2660,7 +2719,12 @@ at the moment path passed as string (basePath) from upper components can be both : new MapSchema(); } schema.setAdditionalProperties(additionalProperties); - } + } else if (additionalProperties != null) { + if (schema == null) { + schema = new Schema(); + } + schema.setAdditionalProperties(additionalProperties); + } if (schema == null) { schema = SchemaTypeUtil.createSchemaByType(node); @@ -2698,6 +2762,9 @@ at the moment path passed as string (basePath) from upper components can be both if(schema.get$ref().startsWith("#/components/schemas")){// it's internal String refName = schema.get$ref().substring(schema.get$ref().lastIndexOf("/")+1); localSchemaRefs.put(refName,location); + } + if(ref.textValue().startsWith("#/components") && !(ref.textValue().startsWith("#/components/schemas"))) { + result.warning(location, "$ref target "+ref.textValue() +" is not of expected type Schema"); } return schema; } else { @@ -2748,7 +2815,7 @@ at the moment path passed as string (basePath) from upper components can be both if (StringUtils.isBlank(schema.getType())) { if ((result.isAllowEmptyStrings() && value != null) || (!result.isAllowEmptyStrings() && !StringUtils.isBlank(value))) { schema.setType(value); - } else { + } else if (result.isInferSchemaType()){ // may have an enum where type can be inferred JsonNode enumNode = node.get("enum"); if (enumNode != null && enumNode.isArray()) { @@ -2793,45 +2860,55 @@ at the moment path passed as string (basePath) from upper components can be both } //sets default value according to the schema type - if (node.get("default") != null && result.isDefaultSchemaTypeObject()) { - if (!StringUtils.isBlank(schema.getType())) { - if (schema.getType().equals("array")) { - ArrayNode array = getArray("default", node, false, location, result); - if (array != null) { - schema.setDefault(array); - } - } else if (schema.getType().equals("string")) { - value = getString("default", node, false, location, result); - if ((result.isAllowEmptyStrings() && value != null) || (!result.isAllowEmptyStrings() && !StringUtils.isBlank(value))) { - try { - schema.setDefault(getDecodedObject(schema, value)); - } catch (ParseException e) { - result.invalidType(location, String.format("default=`%s`", e.getMessage()), - schema.getFormat(), node); - } - } - } else if (schema.getType().equals("boolean")) { - bool = getBoolean("default", node, false, location, result); - if (bool != null) { - schema.setDefault(bool); - } - } else if (schema.getType().equals("object")) { - Object object = getObject("default", node, false, location, result); - if (object != null) { - schema.setDefault(object); - } - } else if (schema.getType().equals("integer")) { - Integer number = getInteger("default", node, false, location, result); - if (number != null) { - schema.setDefault(number); - } - } else if (schema.getType().equals("number")) { - BigDecimal number = getBigDecimal("default", node, false, location, result); - if (number != null) { - schema.setDefault(number); - } - } - } + if (node.get("default") != null && result.isInferSchemaType()) { + if (!StringUtils.isBlank(schema.getType())) { + if (schema.getType().equals("array")) { + ArrayNode array = getArray("default", node, false, location, result); + if (array != null) { + schema.setDefault(array); + } + } else if (schema.getType().equals("string")) { + value = getString("default", node, false, location, result); + if ((result.isAllowEmptyStrings() && value != null) || (!result.isAllowEmptyStrings() && !StringUtils.isBlank(value))) { + try { + schema.setDefault(getDecodedObject(schema, value)); + } catch (ParseException e) { + result.invalidType(location, String.format("default=`%s`", e.getMessage()), + schema.getFormat(), node); + } + } + } else if (schema.getType().equals("boolean")) { + bool = getBoolean("default", node, false, location, result); + if (bool != null) { + schema.setDefault(bool); + } + } else if (schema.getType().equals("object")) { + Object object = getObject("default", node, false, location, result); + if (object != null) { + schema.setDefault(object); + } + } else if (schema.getType().equals("integer")) { + Integer number = getInteger("default", node, false, location, result); + if (number != null) { + schema.setDefault(number); + } + } else if (schema.getType().equals("number")) { + BigDecimal number = getBigDecimal("default", node, false, location, result); + if (number != null) { + schema.setDefault(number); + } + } + } else { + Object defaultObject = getAnyType("default", node, location, result); + if (defaultObject != null) { + schema.setDefault(defaultObject); + } + } + } else if (node.get("default") != null) { + Object defaultObject = getAnyType("default", node, location, result); + if (defaultObject != null) { + schema.setDefault(defaultObject); + } }else{ schema.setDefault(null); } @@ -3161,6 +3238,9 @@ public Example getExample(ObjectNode node, String location, ParseResult result) } else { example.set$ref(ref.textValue()); } + if(ref.textValue().startsWith("#/components") && !(ref.textValue().startsWith("#/components/examples"))) { + result.warning(location, "$ref target "+ref.textValue() +" is not of expected type Examples"); + } if(result.isOpenapi31()){ String value = getString("summary", node, false, location, result); if (StringUtils.isNotBlank(value)) { @@ -3300,6 +3380,9 @@ public ApiResponse getResponse(ObjectNode node, String location, ParseResult res } else { apiResponse.set$ref(ref.textValue()); } + if(ref.textValue().startsWith("#/components") && !(ref.textValue().startsWith("#/components/responses"))) { + result.warning(location, "$ref target "+ref.textValue() +" is not of expected type Response"); + } if(result.isOpenapi31()){ String value = getString("description", node, false, location, result); if (StringUtils.isNotBlank(value)) { @@ -3321,7 +3404,7 @@ public ApiResponse getResponse(ObjectNode node, String location, ParseResult res ObjectNode headerObject = getObject("headers", node, false, location, result); if (headerObject != null) { - Map headers = getHeaders(headerObject, location, result, false); + Map headers = getHeaders(headerObject, String.format("%s.%s", location, "headers"), result, false); if (headers != null && headers.size() > 0) { apiResponse.setHeaders(headers); } @@ -3329,7 +3412,7 @@ public ApiResponse getResponse(ObjectNode node, String location, ParseResult res ObjectNode linksObj = getObject("links", node, false, location, result); if (linksObj != null) { - Map links = getLinks(linksObj, location, result, false); + Map links = getLinks(linksObj, String.format("%s.%s", location, "links"), result, false); if (links != null && links.size() > 0) { apiResponse.setLinks(links); } @@ -3550,6 +3633,9 @@ public RequestBody getRequestBody(ObjectNode node, String location, ParseResult } else { body.set$ref(ref.textValue()); } + if(ref.textValue().startsWith("#/components") && !(ref.textValue().startsWith("#/components/requestBodies"))) { + result.warning(location, "$ref target "+ref.textValue() +" is not of expected type RequestBody"); + } if (result.isOpenapi31()) { String desc = getString("description", node, false, location, result); if (StringUtils.isNotBlank(desc)) { @@ -3652,16 +3738,37 @@ protected static List getJsonSchemaParserExtensions(C return extensions; } - public Schema getJsonSchema(ObjectNode node, String location, ParseResult result) { - if (node == null) { + public Schema getJsonSchema(JsonNode jsonNode, String location, ParseResult result) { + if (jsonNode == null) { return null; } Schema schema = null; - + Boolean boolValue = null; + if (jsonNode.getNodeType().equals(JsonNodeType.BOOLEAN)) { + boolValue = jsonNode.asBoolean(); + } else if (jsonNode.getNodeType().equals(JsonNodeType.STRING)) { + String stringValue = jsonNode.textValue(); + if ("true".equalsIgnoreCase(stringValue) || "false".equalsIgnoreCase(stringValue)) { + boolValue = Boolean.parseBoolean(stringValue); + } else { + result.invalidType(location, "", "object", jsonNode); + return null; + } + } + if (boolValue != null) { + return new JsonSchema().booleanSchemaValue(boolValue); + } + ObjectNode node = null; + if (jsonNode.isObject()) { + node = (ObjectNode) jsonNode; + } else { + result.invalidType(location, "", "object", jsonNode); + return null; + } ArrayNode oneOfArray = getArray("oneOf", node, false, location, result); ArrayNode allOfArray = getArray("allOf", node, false, location, result); ArrayNode anyOfArray = getArray("anyOf", node, false, location, result); - ObjectNode itemsNode = getObject("items", node, false, location, result); + JsonNode itemsNode = getObjectOrBoolean("items", node, false, location, result); if ((allOfArray != null) || (anyOfArray != null) || (oneOfArray != null)) { JsonSchema composedSchema = new JsonSchema(); @@ -3669,109 +3776,50 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result if (allOfArray != null) { for (JsonNode n : allOfArray) { - if (n.isObject()) { - schema = getJsonSchema((ObjectNode) n, location, result); - composedSchema.addAllOfItem(schema); - } + schema = getJsonSchema(n, location, result); + composedSchema.addAllOfItem(schema); } schema = composedSchema; } if (anyOfArray != null) { for (JsonNode n : anyOfArray) { - if (n.isObject()) { - schema = getJsonSchema((ObjectNode) n, location, result); - composedSchema.addAnyOfItem(schema); - } + schema = getJsonSchema(n, location, result); + composedSchema.addAnyOfItem(schema); } schema = composedSchema; } if (oneOfArray != null) { for (JsonNode n : oneOfArray) { - if (n.isObject()) { - schema = getJsonSchema((ObjectNode) n, location, result); - composedSchema.addOneOfItem(schema); - } + schema = getJsonSchema(n, location, result); + composedSchema.addOneOfItem(schema); } schema = composedSchema; } } - if (itemsNode != null && result.isDefaultSchemaTypeObject()) { - ArraySchema items = new ArraySchema(); - if (itemsNode.getNodeType().equals(JsonNodeType.OBJECT)) { - items.setItems(getJsonSchema(itemsNode, location, result)); - } else if (itemsNode.getNodeType().equals(JsonNodeType.ARRAY)) { - for (JsonNode n : itemsNode) { - if (n.isValueNode()) { - items.setItems(getJsonSchema(itemsNode, location, result)); - } - } - } - schema = items; - }else if (itemsNode != null) { - JsonSchema items = new JsonSchema(); - if (itemsNode.getNodeType().equals(JsonNodeType.OBJECT)) { - items.setItems(getJsonSchema(itemsNode, location, result)); - } else if (itemsNode.getNodeType().equals(JsonNodeType.ARRAY)) { - for (JsonNode n : itemsNode) { - if (n.isValueNode()) { - items.setItems(getJsonSchema(itemsNode, location, result)); - } - } - } - schema = items; - } - - Boolean additionalPropertiesBoolean = getBoolean("additionalProperties", node, false, location, result); - - ObjectNode additionalPropertiesObject = - additionalPropertiesBoolean == null - ? getObject("additionalProperties", node, false, location, result) - : null; - - Object additionalProperties = - additionalPropertiesObject != null - ? getJsonSchema(additionalPropertiesObject, location, result) - : additionalPropertiesBoolean; - - - if (additionalProperties != null && result.isDefaultSchemaTypeObject()) { - if (schema == null) { - schema = - additionalProperties.equals(Boolean.FALSE) - ? new ObjectSchema() - : new MapSchema(); - } - schema.setAdditionalProperties(additionalProperties); - }else if (additionalProperties != null) { - if (schema == null) { - schema = new JsonSchema(); - } - schema.setAdditionalProperties(additionalProperties); - } - - Boolean unevaluatedPropertiesBoolean = getBoolean("unevaluatedProperties", node, false, location, result); - - ObjectNode unevaluatedPropertiesObject = - additionalPropertiesBoolean == null - ? getObject("unevaluatedProperties", node, false, location, result) - : null; - - Object unevaluatedProperties = - unevaluatedPropertiesObject != null - ? getJsonSchema(unevaluatedPropertiesObject, location, result) - : unevaluatedPropertiesBoolean; + if (itemsNode != null) { + Schema items = new JsonSchema(); + items.setItems(getJsonSchema(itemsNode, location, result)); + schema = items; + } + JsonNode additionalProperties = getObjectOrBoolean("additionalProperties", node, false, location, result); + if (additionalProperties != null) { + Schema additionalPropertiesSchema = getJsonSchema(additionalProperties, location, result); + if (schema == null) { + schema = new JsonSchema(); + } + schema.setAdditionalProperties(additionalPropertiesSchema); + } - if (unevaluatedProperties != null) { - if (schema == null) { - schema = - unevaluatedProperties.equals(Boolean.FALSE) - ? new ObjectSchema() - : new JsonSchema(); - } - schema.setUnevaluatedProperties(unevaluatedProperties); - } + JsonNode unevaluatedProperties = getObjectOrBoolean("unevaluatedProperties", node, false, location, result); + if (unevaluatedProperties != null) { + Schema unevaluatedPropertiesSchema = getJsonSchema(unevaluatedProperties, location, result); + if (schema == null) { + schema = new JsonSchema(); + } + schema.setUnevaluatedProperties(unevaluatedPropertiesSchema); + } if (schema == null) { schema = new JsonSchema(); @@ -3806,6 +3854,9 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result } else { schema.set$ref(ref.asText()); } + if(ref.textValue().startsWith("#/components") && !(ref.textValue().startsWith("#/components/schemas"))) { + result.warning(location, "$ref target "+ref.textValue() +" is not of expected type Schema"); + } } else { result.invalidType(location, "$ref", "string", node); } @@ -3817,9 +3868,7 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result Integer integer; if (node.get("default") != null) { - if(result.isDefaultSchemaTypeObject()) { - schema.setDefault(getAnyType("default", node, location, result)); - } + schema.setDefault(getAnyType("default", node, location, result)); } BigDecimal bigDecimal = getBigDecimal("exclusiveMaximum", node, false, location, result); @@ -3892,7 +3941,7 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result } } - ObjectNode notObj = getObject("not", node, false, location, result); + JsonNode notObj = getObjectOrBoolean("not", node, false, location, result); if (notObj != null) { Schema not = getJsonSchema(notObj, location, result); if (not != null) { @@ -3900,7 +3949,7 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result } } - ObjectNode contentSchemaObj = getObject("contentSchema", node, false, location, result); + JsonNode contentSchemaObj = getObjectOrBoolean("contentSchema", node, false, location, result); if (contentSchemaObj != null) { Schema contentSchema = getJsonSchema(contentSchemaObj, location, result); if (contentSchema != null) { @@ -3908,7 +3957,7 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result } } - ObjectNode propertyNamesObj = getObject("propertyNames", node, false, location, result); + JsonNode propertyNamesObj = getObjectOrBoolean("propertyNames", node, false, location, result); if (propertyNamesObj != null) { Schema propertyNames = getJsonSchema(propertyNamesObj, location, result); if (propertyNames != null) { @@ -3916,7 +3965,7 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result } } - ObjectNode ifObj = getObject("if", node, false, location, result); + JsonNode ifObj = getObjectOrBoolean("if", node, false, location, result); if (ifObj != null) { Schema _if = getJsonSchema(ifObj, location, result); if (_if != null) { @@ -3924,7 +3973,7 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result } } - ObjectNode thenObj = getObject("then", node, false, location, result); + JsonNode thenObj = getObjectOrBoolean("then", node, false, location, result); if (thenObj != null) { Schema _then = getJsonSchema(thenObj, location, result); if (_then != null) { @@ -3932,7 +3981,7 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result } } - ObjectNode elseObj = getObject("else", node, false, location, result); + JsonNode elseObj = getObjectOrBoolean("else", node, false, location, result); if (elseObj != null) { Schema _else = getJsonSchema(elseObj, location, result); if (_else != null) { @@ -3940,7 +3989,7 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result } } - ObjectNode unevaluatedItems = getObject("unevaluatedItems", node, false, location, result); + JsonNode unevaluatedItems = getObjectOrBoolean("unevaluatedItems", node, false, location, result); if (unevaluatedItems != null) { Schema unevaluatedItemsSchema = getJsonSchema(unevaluatedItems, location, result); if (unevaluatedItemsSchema != null) { @@ -3977,44 +4026,40 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result Map dependentSchemasList = new LinkedHashMap<>(); ObjectNode dependentSchemasObj = getObject("dependentSchemas", node, false, location, result); - Schema dependentSchemas = null; - - Set dependentSchemasKeys = getKeys(dependentSchemasObj); - for (String name : dependentSchemasKeys) { - JsonNode dependentSchemasValue = dependentSchemasObj.get(name); - if (!dependentSchemasValue.getNodeType().equals(JsonNodeType.OBJECT)) { - result.invalidType(location, "dependentSchemas", "object", dependentSchemasValue); - } else { - if (dependentSchemasObj != null) { - dependentSchemas = getJsonSchema((ObjectNode) dependentSchemasValue, location, result); - if (dependentSchemas != null) { - dependentSchemasList.put(name, dependentSchemas); - } - } - } - } - if (dependentSchemasObj != null) { - schema.setDependentSchemas(dependentSchemasList); - } + if (dependentSchemasObj != null) { + Schema dependentSchemas = null; + + Set dependentSchemasKeys = getKeys(dependentSchemasObj); + for (String name : dependentSchemasKeys) { + JsonNode dependentSchemasValue = dependentSchemasObj.get(name); + dependentSchemas = getJsonSchema(dependentSchemasValue, location, result); + if (dependentSchemas != null) { + dependentSchemasList.put(name, dependentSchemas); + } + } + if (dependentSchemasObj != null) { + schema.setDependentSchemas(dependentSchemasList); + } + } //prefixItems ArrayNode prefixItemsArray = getArray("prefixItems", node, false, location, result); if(prefixItemsArray != null) { - Schema prefixItems = new Schema(); + Schema prefixItems = new JsonSchema(); List prefixItemsList = new ArrayList<>(); for (JsonNode n : prefixItemsArray) { - if (n.isObject()) { - prefixItems = getJsonSchema((ObjectNode) n, location, result); - prefixItemsList.add(prefixItems); - } + prefixItems = getJsonSchema(n, location, result); + if (prefixItems != null) { + prefixItemsList.add(prefixItems); + } } if (prefixItemsList.size() > 0) { schema.setPrefixItems(prefixItemsList); } } - ObjectNode containsObj = getObject("contains", node, false, location, result); + JsonNode containsObj = getObjectOrBoolean("contains", node, false, location, result); if (containsObj != null) { Schema contains = getJsonSchema(containsObj, location, result); if (contains != null) { @@ -4029,16 +4074,12 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result Set keys = getKeys(propertiesObj); for (String name : keys) { JsonNode propertyValue = propertiesObj.get(name); - if (!propertyValue.getNodeType().equals(JsonNodeType.OBJECT)) { - result.invalidType(location, "properties", "object", propertyValue); - } else { - if (propertiesObj != null) { - property = getJsonSchema((ObjectNode) propertyValue, location, result); - if (property != null) { - properties.put(name, property); - } - } - } + if (propertiesObj != null) { + property = getJsonSchema(propertyValue, location, result); + if (property != null) { + properties.put(name, property); + } + } } if (propertiesObj != null) { schema.setProperties(properties); @@ -4051,16 +4092,12 @@ public Schema getJsonSchema(ObjectNode node, String location, ParseResult result Set patternKeys = getKeys(patternPropertiesObj); for (String name : patternKeys) { JsonNode propertyValue = patternPropertiesObj.get(name); - if (!propertyValue.getNodeType().equals(JsonNodeType.OBJECT)) { - result.invalidType(location, "patternProperties", "object", propertyValue); - } else { - if (patternPropertiesObj != null) { - patternProperty = getJsonSchema((ObjectNode) propertyValue, location, result); - if (patternProperty != null) { - patternProperties.put(name, patternProperty); - } - } - } + if (patternPropertiesObj != null) { + patternProperty = getJsonSchema(propertyValue, location, result); + if (patternProperty != null) { + patternProperties.put(name, patternProperty); + } + } } if (patternPropertiesObj != null) { schema.setPatternProperties(patternProperties); @@ -4143,20 +4180,20 @@ public static class ParseResult { private List reserved = new ArrayList<>(); private boolean validateInternalRefs; - private boolean defaultSchemaTypeObject = true; + private boolean inferSchemaType = true; private boolean openapi31 = false; private boolean oaiAuthor = false; - public boolean isDefaultSchemaTypeObject() { - return defaultSchemaTypeObject; + public boolean isInferSchemaType() { + return inferSchemaType; } - public void setDefaultSchemaTypeObject(boolean defaultSchemaTypeObject) { - this.defaultSchemaTypeObject = defaultSchemaTypeObject; + public void setInferSchemaType(boolean inferSchemaType) { + this.inferSchemaType = inferSchemaType; } - public ParseResult defaultSchemaTypeObject(boolean defaultSchemaTypeObject) { - this.defaultSchemaTypeObject = defaultSchemaTypeObject; + public ParseResult inferSchemaType(boolean inferSchemaType) { + this.inferSchemaType = inferSchemaType; return this; } diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OAI31DeserializationTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OAI31DeserializationTest.java index c0b10cd8ef..f4829123ba 100644 --- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OAI31DeserializationTest.java +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OAI31DeserializationTest.java @@ -132,7 +132,7 @@ public void testBasicOAS30_With31Fields() { assertEquals(openAPI.getComponents().getSchemas().get("ArrayWithoutItems").getType(),"array"); assertNull(openAPI.getComponents().getSchemas().get("ArrayWithoutItems").getItems()); assertTrue(result.getMessages().contains("attribute components.schemas.ArrayWithoutItems.items is missing")); - //Type object with items: not allowed, it will internally create an array (New Option setDefaultSchemaTypeObject ) + //Type object with items: not allowed, it will internally create an array (New Option setInferSchemaType ) assertNotEquals(openAPI.getComponents().getSchemas().get("ItemsWithoutArrayType").getType(),"object"); assertEquals(openAPI.getComponents().getSchemas().get("ItemsWithoutArrayType").getType(),"array"); assertNotNull(openAPI.getComponents().getSchemas().get("ItemsWithoutArrayType").getItems()); @@ -149,12 +149,10 @@ public void testBasicOAS30_With31Fields() { //exclusiveMaximum-exclusiveMinimum are boolean in 3.0 assertNull(openAPI.getComponents().getSchemas().get("Pets").getExclusiveMaximum()); assertNull(openAPI.getComponents().getSchemas().get("Pets").getExclusiveMinimum()); - //Null type - assertNull(openAPI.getComponents().getSchemas().get("Pets").getTypes()); + assertEquals(openAPI.getComponents().getSchemas().get("Pets").getTypes().iterator().next(), "array"); //default value independence assertNull(openAPI.getComponents().getSchemas().get("Pets").getDefault()); - //not setting the type by default - assertNull(openAPI.getComponents().getSchemas().get("MapAnyValue").getTypes()); + assertEquals(openAPI.getComponents().getSchemas().get("MapAnyValue").getTypes().iterator().next(), "object"); //Not webhooks assertTrue(result.getMessages().contains("attribute webhooks is unexpected")); } @@ -595,8 +593,8 @@ public void testSiblingsReferenceJSONSchema5() { assertTrue(profile.getUnevaluatedProperties() instanceof Schema); assertTrue(((Schema)profile.getUnevaluatedProperties()).getTypes().contains("object")); assertNotNull(patientPersonSchema.getUnevaluatedProperties()); - assertTrue(patientPersonSchema.getUnevaluatedProperties() instanceof Boolean); - assertFalse(((Boolean)patientPersonSchema.getUnevaluatedProperties()).booleanValue()); + assertTrue(patientPersonSchema.getUnevaluatedProperties() instanceof Schema); + assertFalse(patientPersonSchema.getUnevaluatedProperties().getBooleanSchemaValue()); //unevaluatedItems assertNotNull(profile.getUnevaluatedItems()); @@ -811,7 +809,7 @@ public void testTuplesJSONSchema() { @Test(description = "Test for not setting the schema type as default") public void testNotDefaultSchemaType() { ParseOptions options = new ParseOptions(); - options.setDefaultSchemaTypeObject(false); + options.setInferSchemaType(false); String defaultSchemaType = "openapi: 3.1.0\n" + "info:\n" + " title: ping test\n" + diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java index 068ae5173b..055c2de20d 100644 --- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/OpenAPIV3ParserTest.java @@ -32,12 +32,10 @@ import java.util.Random; import java.util.Set; -import io.swagger.v3.parser.util.DeserializationUtils; import io.swagger.v3.parser.util.SchemaTypeUtil; import org.apache.commons.io.FileUtils; import org.hamcrest.CoreMatchers; import org.testng.Assert; -import org.testng.AssertJUnit; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -88,10 +86,30 @@ public class OpenAPIV3ParserTest { protected int serverPort = getDynamicPort(); protected WireMockServer wireMockServer; + @Test + public void testIssue1758() throws Exception{ + ParseOptions options = new ParseOptions(); + SwaggerParseResult result = new OpenAPIV3Parser().readLocation("src/test/resources/issue1758.yaml", null, options); + + Assert.assertNotNull(result); + Assert.assertNotNull(result.getOpenAPI()); + assertEquals(result.getMessages().size(),9); + assertTrue(result.getMessages().contains("paths.'/path1'.$ref target #/components/schemas/xFoo is not of expected type PathItem")); + assertTrue(result.getMessages().contains("paths.'/foo'(get).parameters.$ref target #/components/schemas/xFoo is not of expected type Parameter/Header")); + assertTrue(result.getMessages().contains("paths.'/foo'(get).responses.default.headers.three.$ref target #/components/schemas/xFoo is not of expected type Header/Parameter")); + assertTrue(result.getMessages().contains("paths.'/foo'(get).requestBody.$ref target #/components/schemas/xFoo is not of expected type RequestBody")); + assertTrue(result.getMessages().contains("paths.'/foo'(get).responses.200.links.user.$ref target #/components/schemas/xFoo is not of expected type Link")); + assertTrue(result.getMessages().contains("paths.'/foo'(get).responses.200.content.'application/json'.schema.$ref target #/components/parameters/pet is not of expected type Schema")); + assertTrue(result.getMessages().contains("paths.'/foo'(get).responses.200.content.'application/json'.examples.one.$ref target #/components/schemas/xFoo is not of expected type Examples")); + assertTrue(result.getMessages().contains("paths.'/foo'(get).responses.400.$ref target #/components/schemas/xFoo is not of expected type Response")); + assertTrue(result.getMessages().contains("paths.'/foo'(get).callbacks.$ref target #/components/schemas/xFoo is not of expected type Callback")); + + } + @Test(description = "Test for not setting the schema type as default") public void testNotDefaultSchemaType() { ParseOptions options = new ParseOptions(); - options.setDefaultSchemaTypeObject(false); + options.setInferSchemaType(false); String defaultSchemaType = "openapi: 3.0.0\n" + "info:\n" + " title: ping test\n" + @@ -185,7 +203,7 @@ public void testNotDefaultSchemaType() { public void testIssue1637_StyleAndContent() throws IOException { ParseOptions options = new ParseOptions(); SwaggerParseResult result = new OpenAPIV3Parser().readLocation("src/test/resources/issue1637.yaml", null, options); - + Assert.assertNull(result.getOpenAPI().getPaths().get("/test").getGet().getParameters().get(0).getStyle()); Assert.assertNull(result.getOpenAPI().getPaths().get("/test").getGet().getParameters().get(0).getExplode()); Assert.assertNotNull(result.getOpenAPI().getPaths().get("/test").getGet().getParameters().get(0).getContent()); @@ -516,7 +534,6 @@ public void testIssueSameRefsDifferentModelValid() { options.setResolveFully(true); final SwaggerParseResult openAPI = parser.readLocation("src/test/resources/same-refs-different-model-valid.yaml", null, options); - Yaml.prettyPrint(openAPI); assertEquals(openAPI.getMessages().size(), 0); } diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/OpenAPIDeserializerTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/OpenAPIDeserializerTest.java index 5b2bb451d3..d63f73c8cc 100644 --- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/OpenAPIDeserializerTest.java +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/OpenAPIDeserializerTest.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Yaml; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.ExternalDocumentation; import io.swagger.v3.oas.models.OpenAPI; @@ -3567,5 +3568,227 @@ private Object[][] getRootNode() throws Exception { return new Object[][]{new Object[]{rootNode}}; } + @Test + public void testIssue1761() { + OpenAPIV3Parser parser = new OpenAPIV3Parser(); + ParseOptions options = new ParseOptions(); + options.setInferSchemaType(false); + SwaggerParseResult result = parser.readLocation("./src/test/resources/issue-1761.yaml", null, options); + + assertEquals(Yaml.pretty(result.getOpenAPI()), "openapi: 3.0.3\n" + + "info:\n" + + " title: openapi 3.0.3 sample spec\n" + + " description: \"sample spec for testing openapi functionality, built from json schema\\\n" + + " \\ tests for draft6\"\n" + + " version: 0.0.1\n" + + "servers:\n" + + "- url: /\n" + + "paths: {}\n" + + "components:\n" + + " schemas:\n" + + " SimpleEnumValidation:\n" + + " enum:\n" + + " - 1\n" + + " - 2\n" + + " - 3\n" + + " HeterogeneousEnumValidation:\n" + + " enum:\n" + + " - 6\n" + + " - foo\n" + + " - []\n" + + " - true\n" + + " - foo: 12\n" + + " HeterogeneousEnumWithNullValidation:\n" + + " enum:\n" + + " - 6\n" + + " - null\n" + + " EnumsInProperties:\n" + + " required:\n" + + " - bar\n" + + " type: object\n" + + " properties:\n" + + " foo:\n" + + " enum:\n" + + " - foo\n" + + " bar:\n" + + " enum:\n" + + " - bar\n" + + " EnumWithEscapedCharacters:\n" + + " enum:\n" + + " - |-\n" + + " foo\n" + + " bar\n" + + " - \"foo\\rbar\"\n" + + " EnumWithFalseDoesNotMatch0:\n" + + " enum:\n" + + " - false\n" + + " EnumWithTrueDoesNotMatch1:\n" + + " enum:\n" + + " - true\n" + + " EnumWith0DoesNotMatchFalse:\n" + + " enum:\n" + + " - 0\n" + + " EnumWith1DoesNotMatchTrue:\n" + + " enum:\n" + + " - 1\n" + + " NulCharactersInStrings:\n" + + " enum:\n" + + " - \"hello\\0there\"\n" + + " x-schema-test-examples:\n" + + " SimpleEnumValidation:\n" + + " OneOfTheEnumIsValid:\n" + + " description: one of the enum is valid\n" + + " data: 1\n" + + " valid: true\n" + + " SomethingElseIsInvalid:\n" + + " description: something else is invalid\n" + + " data: 4\n" + + " valid: false\n" + + " HeterogeneousEnumValidation:\n" + + " OneOfTheEnumIsValid:\n" + + " description: one of the enum is valid\n" + + " data: []\n" + + " valid: true\n" + + " SomethingElseIsInvalid:\n" + + " description: something else is invalid\n" + + " valid: false\n" + + " ObjectsAreDeepCompared:\n" + + " description: objects are deep compared\n" + + " data:\n" + + " foo: false\n" + + " valid: false\n" + + " ValidObjectMatches:\n" + + " description: valid object matches\n" + + " data:\n" + + " foo: 12\n" + + " valid: true\n" + + " ExtraPropertiesInObjectIsInvalid:\n" + + " description: extra properties in object is invalid\n" + + " data:\n" + + " foo: 12\n" + + " boo: 42\n" + + " valid: false\n" + + " HeterogeneousEnumWithNullValidation:\n" + + " NullIsValid:\n" + + " description: null is valid\n" + + " valid: true\n" + + " NumberIsValid:\n" + + " description: number is valid\n" + + " data: 6\n" + + " valid: true\n" + + " SomethingElseIsInvalid:\n" + + " description: something else is invalid\n" + + " data: test\n" + + " valid: false\n" + + " EnumsInProperties:\n" + + " BothPropertiesAreValid:\n" + + " description: both properties are valid\n" + + " data:\n" + + " foo: foo\n" + + " bar: bar\n" + + " valid: true\n" + + " WrongFooValue:\n" + + " description: wrong foo value\n" + + " data:\n" + + " foo: foot\n" + + " bar: bar\n" + + " valid: false\n" + + " WrongBarValue:\n" + + " description: wrong bar value\n" + + " data:\n" + + " foo: foo\n" + + " bar: bart\n" + + " valid: false\n" + + " MissingOptionalPropertyIsValid:\n" + + " description: missing optional property is valid\n" + + " data:\n" + + " bar: bar\n" + + " valid: true\n" + + " MissingRequiredPropertyIsInvalid:\n" + + " description: missing required property is invalid\n" + + " data:\n" + + " foo: foo\n" + + " valid: false\n" + + " MissingAllPropertiesIsInvalid:\n" + + " description: missing all properties is invalid\n" + + " data: {}\n" + + " valid: false\n" + + " EnumWithEscapedCharacters:\n" + + " Member1IsValid:\n" + + " description: member 1 is valid\n" + + " data: |-\n" + + " foo\n" + + " bar\n" + + " valid: true\n" + + " Member2IsValid:\n" + + " description: member 2 is valid\n" + + " data: \"foo\\rbar\"\n" + + " valid: true\n" + + " AnotherStringIsInvalid:\n" + + " description: another string is invalid\n" + + " data: abc\n" + + " valid: false\n" + + " EnumWithFalseDoesNotMatch0:\n" + + " FalseIsValid:\n" + + " description: false is valid\n" + + " data: false\n" + + " valid: true\n" + + " IntegerZeroIsInvalid:\n" + + " description: integer zero is invalid\n" + + " data: 0\n" + + " valid: false\n" + + " FloatZeroIsInvalid:\n" + + " description: float zero is invalid\n" + + " data: 0.0\n" + + " valid: false\n" + + " EnumWithTrueDoesNotMatch1:\n" + + " TrueIsValid:\n" + + " description: true is valid\n" + + " data: true\n" + + " valid: true\n" + + " IntegerOneIsInvalid:\n" + + " description: integer one is invalid\n" + + " data: 1\n" + + " valid: false\n" + + " FloatOneIsInvalid:\n" + + " description: float one is invalid\n" + + " data: 1.0\n" + + " valid: false\n" + + " EnumWith0DoesNotMatchFalse:\n" + + " FalseIsInvalid:\n" + + " description: false is invalid\n" + + " data: false\n" + + " valid: false\n" + + " IntegerZeroIsValid:\n" + + " description: integer zero is valid\n" + + " data: 0\n" + + " valid: true\n" + + " FloatZeroIsValid:\n" + + " description: float zero is valid\n" + + " data: 0.0\n" + + " valid: true\n" + + " EnumWith1DoesNotMatchTrue:\n" + + " TrueIsInvalid:\n" + + " description: true is invalid\n" + + " data: true\n" + + " valid: false\n" + + " IntegerOneIsValid:\n" + + " description: integer one is valid\n" + + " data: 1\n" + + " valid: true\n" + + " FloatOneIsValid:\n" + + " description: float one is valid\n" + + " data: 1.0\n" + + " valid: true\n" + + " NulCharactersInStrings:\n" + + " MatchStringWithNul:\n" + + " description: match string with nul\n" + + " data: \"hello\\0there\"\n" + + " valid: true\n" + + " DoNotMatchStringLackingNul:\n" + + " description: do not match string lacking nul\n" + + " data: hellothere\n" + + " valid: false\n"); + } } diff --git a/modules/swagger-parser-v3/src/test/resources/issue-1761.yaml b/modules/swagger-parser-v3/src/test/resources/issue-1761.yaml new file mode 100644 index 0000000000..bda06caea8 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue-1761.yaml @@ -0,0 +1,214 @@ +openapi: 3.0.3 +info: + title: openapi 3.0.3 sample spec + description: sample spec for testing openapi functionality, built from json schema + tests for draft6 + version: 0.0.1 +paths: {} +components: + schemas: + SimpleEnumValidation: + enum: + - 1 + - 2 + - 3 + HeterogeneousEnumValidation: + enum: + - 6 + - foo + - [] + - true + - foo: 12 + HeterogeneousEnumWithNullValidation: + enum: + - 6 + - null + EnumsInProperties: + type: object + properties: + foo: + enum: + - foo + bar: + enum: + - bar + required: + - bar + EnumWithEscapedCharacters: + enum: + - 'foo + + bar' + - "foo\rbar" + EnumWithFalseDoesNotMatch0: + enum: + - false + EnumWithTrueDoesNotMatch1: + enum: + - true + EnumWith0DoesNotMatchFalse: + enum: + - 0 + EnumWith1DoesNotMatchTrue: + enum: + - 1 + NulCharactersInStrings: + enum: + - "hello\0there" + x-schema-test-examples: + SimpleEnumValidation: + OneOfTheEnumIsValid: + description: one of the enum is valid + data: 1 + valid: true + SomethingElseIsInvalid: + description: something else is invalid + data: 4 + valid: false + HeterogeneousEnumValidation: + OneOfTheEnumIsValid: + description: one of the enum is valid + data: [] + valid: true + SomethingElseIsInvalid: + description: something else is invalid + data: null + valid: false + ObjectsAreDeepCompared: + description: objects are deep compared + data: + foo: false + valid: false + ValidObjectMatches: + description: valid object matches + data: + foo: 12 + valid: true + ExtraPropertiesInObjectIsInvalid: + description: extra properties in object is invalid + data: + foo: 12 + boo: 42 + valid: false + HeterogeneousEnumWithNullValidation: + NullIsValid: + description: null is valid + data: null + valid: true + NumberIsValid: + description: number is valid + data: 6 + valid: true + SomethingElseIsInvalid: + description: something else is invalid + data: test + valid: false + EnumsInProperties: + BothPropertiesAreValid: + description: both properties are valid + data: + foo: foo + bar: bar + valid: true + WrongFooValue: + description: wrong foo value + data: + foo: foot + bar: bar + valid: false + WrongBarValue: + description: wrong bar value + data: + foo: foo + bar: bart + valid: false + MissingOptionalPropertyIsValid: + description: missing optional property is valid + data: + bar: bar + valid: true + MissingRequiredPropertyIsInvalid: + description: missing required property is invalid + data: + foo: foo + valid: false + MissingAllPropertiesIsInvalid: + description: missing all properties is invalid + data: {} + valid: false + EnumWithEscapedCharacters: + Member1IsValid: + description: member 1 is valid + data: 'foo + + bar' + valid: true + Member2IsValid: + description: member 2 is valid + data: "foo\rbar" + valid: true + AnotherStringIsInvalid: + description: another string is invalid + data: abc + valid: false + EnumWithFalseDoesNotMatch0: + FalseIsValid: + description: false is valid + data: false + valid: true + IntegerZeroIsInvalid: + description: integer zero is invalid + data: 0 + valid: false + FloatZeroIsInvalid: + description: float zero is invalid + data: 0.0 + valid: false + EnumWithTrueDoesNotMatch1: + TrueIsValid: + description: true is valid + data: true + valid: true + IntegerOneIsInvalid: + description: integer one is invalid + data: 1 + valid: false + FloatOneIsInvalid: + description: float one is invalid + data: 1.0 + valid: false + EnumWith0DoesNotMatchFalse: + FalseIsInvalid: + description: false is invalid + data: false + valid: false + IntegerZeroIsValid: + description: integer zero is valid + data: 0 + valid: true + FloatZeroIsValid: + description: float zero is valid + data: 0.0 + valid: true + EnumWith1DoesNotMatchTrue: + TrueIsInvalid: + description: true is invalid + data: true + valid: false + IntegerOneIsValid: + description: integer one is valid + data: 1 + valid: true + FloatOneIsValid: + description: float one is valid + data: 1.0 + valid: true + NulCharactersInStrings: + MatchStringWithNul: + description: match string with nul + data: "hello\0there" + valid: true + DoNotMatchStringLackingNul: + description: do not match string lacking nul + data: hellothere + valid: false \ No newline at end of file diff --git a/modules/swagger-parser-v3/src/test/resources/issue1758.yaml b/modules/swagger-parser-v3/src/test/resources/issue1758.yaml new file mode 100644 index 0000000000..2e0db04dfe --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/issue1758.yaml @@ -0,0 +1,55 @@ +openapi: 3.0.3 +info: + title: Missing validation rule for schemas in Headers. + version: 1.0.0 +servers: +- url: / +paths: + /path1: + $ref: '#/components/schemas/xFoo' + /foo: + get: + description: ok + parameters: + - $ref: '#/components/schemas/xFoo' + requestBody: + $ref: '#/components/schemas/xFoo' + responses: + default: + description: ok + headers: + three: + $ref: '#/components/schemas/xFoo' + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/parameters/pet' + examples: + one: + $ref: '#/components/schemas/xFoo' + links: + user: + $ref: '#/components/schemas/xFoo' + '400': + $ref: '#/components/schemas/xFoo' + callbacks: + mainHook: + $ref: "#/components/schemas/xFoo" +components: + schemas: + xFoo: + type: string + description: This isn't validated correctly + parameters: + pet: + name: X-pet + in: header + required: false + schema: + type: string + format: uuid + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 20f8166826..985cf2294e 100644 --- a/pom.xml +++ b/pom.xml @@ -377,6 +377,7 @@ modules/swagger-parser-v3 modules/swagger-parser-v2-converter modules/swagger-parser + modules/swagger-parser-cli @@ -405,7 +406,7 @@ 1.0.61 2.11.0 1.7.30 - 2.2.1 + 2.2.2-SNAPSHOT 1.6.6 4.13.2 6.14.2 From d49fce212ad32e7de46aaa0955a64db993439e34 Mon Sep 17 00:00:00 2001 From: gracekarina Date: Mon, 18 Jul 2022 11:27:46 -0500 Subject: [PATCH 2/2] fix relative parent --- modules/swagger-parser-cli/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/swagger-parser-cli/pom.xml b/modules/swagger-parser-cli/pom.xml index b27ac4302e..6dc74a4333 100644 --- a/modules/swagger-parser-cli/pom.xml +++ b/modules/swagger-parser-cli/pom.xml @@ -6,6 +6,7 @@ swagger-parser-project io.swagger.parser.v3 2.1.2-SNAPSHOT + ../.. 4.0.0