From 8a25594a64b1c13030e482d22632dd132bcf5ce0 Mon Sep 17 00:00:00 2001 From: gracekarina Date: Thu, 26 May 2022 15:28:33 -0500 Subject: [PATCH 1/3] fix to remove schema duplicates when resolveFully and then flatten --- .../v3/parser/util/InlineModelResolver.java | 86 +++++--- .../parser/util/InlineModelResolverTest.java | 3 +- .../java/io/swagger/parser/OpenAPIParser.java | 1 - .../io/swagger/parser/OpenAPIParserTest.java | 18 ++ .../src/test/resources/petStore1599.yaml | 199 ++++++++++++++++++ 5 files changed, 277 insertions(+), 30 deletions(-) create mode 100644 modules/swagger-parser/src/test/resources/petStore1599.yaml diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/InlineModelResolver.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/InlineModelResolver.java index 941bb68795..683ed41a49 100644 --- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/InlineModelResolver.java +++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/InlineModelResolver.java @@ -1,9 +1,6 @@ package io.swagger.v3.parser.util; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -102,10 +99,20 @@ private void flattenBody(String pathname, RequestBody body) String genericName = pathBody(pathname); if (model.getProperties() != null && model.getProperties().size() > 0) { flattenProperties(model.getProperties(), pathname); - String modelName = resolveModelName(model.getTitle(), genericName); - mediaType.setSchema(new Schema().$ref(modelName)); - addGenerated(modelName, model); - openAPI.getComponents().addSchemas(modelName, model); + if(openAPI.getComponents().getSchemas() == null ) { + createBodySchemaReference(mediaType, model, genericName); + } else if (!openAPI.getComponents().getSchemas().containsValue(model)) { + createBodySchemaReference(mediaType, model, genericName); + } else { + //Look at Components.schemas and use the reference name + String modelName = ""; + for (Map.Entry component : openAPI.getComponents().getSchemas().entrySet()) { + if (component.getValue().equals(model)) { + modelName = component.getKey(); + } + } + mediaType.setSchema(new Schema().$ref(modelName)); + } } else if (model instanceof ComposedSchema) { flattenComposedSchema(model, pathname); if (model.get$ref() == null) { @@ -145,6 +152,13 @@ private void flattenBody(String pathname, RequestBody body) } } + private void createBodySchemaReference(MediaType mediaType, Schema model, String genericName) { + String modelName = resolveModelName(model.getTitle(), genericName); + mediaType.setSchema(new Schema().$ref(modelName)); + addGenerated(modelName, model); + openAPI.getComponents().addSchemas(modelName, model); + } + private void flattenParams(String pathname, List parameters) { if (parameters == null){ @@ -480,32 +494,20 @@ public void flattenProperties(Map properties, String path) { for (String key : properties.keySet()) { Schema property = properties.get(key); if (isObjectSchema(property) && property.getProperties() != null && property.getProperties().size() > 0) { - String modelName = resolveModelName(property.getTitle(), path + "_" + key); - Schema model = createModelFromProperty(property, modelName); - String existing = matchGenerated(model); - if (existing != null) { - propsToUpdate.put(key, new Schema().$ref(existing)); - } else { - propsToUpdate.put(key, new Schema().$ref(RefType.SCHEMAS.getInternalPrefix()+modelName)); - modelsToAdd.put(modelName, model); - addGenerated(modelName, model); - openAPI.getComponents().addSchemas(modelName, model); + if(openAPI.getComponents().getSchemas() == null){ + createSchemaProperty(path, propsToUpdate, modelsToAdd, key, property); + }else if (!openAPI.getComponents().getSchemas().containsValue(property)) { + createSchemaProperty(path, propsToUpdate, modelsToAdd, key, property); } } else if (property instanceof ArraySchema) { ArraySchema ap = (ArraySchema) property; Schema inner = ap.getItems(); if (isObjectSchema(inner)) { if (inner.getProperties() != null && inner.getProperties().size() > 0) { - flattenProperties(inner.getProperties(), path); - String modelName = resolveModelName(inner.getTitle(), path + "_" + key); - Schema innerModel = createModelFromProperty(inner, modelName); - String existing = matchGenerated(innerModel); - if (existing != null) { - ap.setItems(new Schema().$ref(existing)); - } else { - ap.setItems(new Schema().$ref(modelName)); - addGenerated(modelName, innerModel); - openAPI.getComponents().addSchemas(modelName, innerModel); + if(openAPI.getComponents().getSchemas() == null) { + createArraySchemaProperty(path, key, ap, inner); + }else if (!openAPI.getComponents().getSchemas().containsValue(inner)) { + createArraySchemaProperty(path, key, ap, inner); } }else if (inner instanceof ComposedSchema && this.flattenComposedSchemas) { flattenComposedSchema(inner,key); @@ -551,6 +553,34 @@ public void flattenProperties(Map properties, String path) { } } + private void createArraySchemaProperty(String path, String key, ArraySchema ap, Schema inner) { + flattenProperties(inner.getProperties(), path); + String modelName = resolveModelName(inner.getTitle(), path + "_" + key); + Schema innerModel = createModelFromProperty(inner, modelName); + String existing = matchGenerated(innerModel); + if (existing != null) { + ap.setItems(new Schema().$ref(existing)); + } else { + ap.setItems(new Schema().$ref(modelName)); + addGenerated(modelName, innerModel); + openAPI.getComponents().addSchemas(modelName, innerModel); + } + } + + private void createSchemaProperty(String path, Map propsToUpdate, Map modelsToAdd, String key, Schema property) { + String modelName = resolveModelName(property.getTitle(), path + "_" + key); + Schema model = createModelFromProperty(property, modelName); + String existing = matchGenerated(model); + if (existing != null) { + propsToUpdate.put(key, new Schema().$ref(existing)); + } else { + propsToUpdate.put(key, new Schema().$ref(RefType.SCHEMAS.getInternalPrefix() + modelName)); + modelsToAdd.put(modelName, model); + addGenerated(modelName, model); + openAPI.getComponents().addSchemas(modelName, model); + } + } + private void flattenComposedSchema(Schema inner, String key) { ComposedSchema composedSchema = (ComposedSchema) inner; diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java index 5b4a60c7b2..40c7d8be93 100644 --- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; +import io.swagger.v3.core.util.Yaml; import org.testng.annotations.Test; import io.swagger.v3.oas.models.Components; @@ -834,7 +835,7 @@ public void resolveInlineArrayRequestBody() throws Exception { .requestBody(new RequestBody() .content(new Content().addMediaType("*/*",new MediaType() .schema(arraySchema)))))); - + new InlineModelResolver().flatten(openAPI); RequestBody body = openAPI.getPaths().get("/hello").getGet().getRequestBody(); diff --git a/modules/swagger-parser/src/main/java/io/swagger/parser/OpenAPIParser.java b/modules/swagger-parser/src/main/java/io/swagger/parser/OpenAPIParser.java index f42633cdce..d1db26f251 100644 --- a/modules/swagger-parser/src/main/java/io/swagger/parser/OpenAPIParser.java +++ b/modules/swagger-parser/src/main/java/io/swagger/parser/OpenAPIParser.java @@ -18,7 +18,6 @@ public SwaggerParseResult readLocation(String url, List auth return output; } } - return output; } diff --git a/modules/swagger-parser/src/test/java/io/swagger/parser/OpenAPIParserTest.java b/modules/swagger-parser/src/test/java/io/swagger/parser/OpenAPIParserTest.java index 23039b9b45..c8c38e68f4 100644 --- a/modules/swagger-parser/src/test/java/io/swagger/parser/OpenAPIParserTest.java +++ b/modules/swagger-parser/src/test/java/io/swagger/parser/OpenAPIParserTest.java @@ -1,5 +1,6 @@ package io.swagger.parser; +import io.swagger.v3.core.util.Yaml; import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.ArraySchema; @@ -11,6 +12,7 @@ import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.parser.OpenAPIV3Parser; import io.swagger.v3.parser.core.models.ParseOptions; import io.swagger.v3.parser.core.models.SwaggerParseResult; import io.swagger.v3.core.util.Json; @@ -31,6 +33,22 @@ public class OpenAPIParserTest { + @Test + public void testIssue_1599() { + OpenAPIParser openAPIParser = new OpenAPIParser(); + ParseOptions options = new ParseOptions(); + options.setResolve(true); + options.setResolveFully(true); + options.setFlatten(true); + SwaggerParseResult swaggerParseResult = openAPIParser.readLocation("petStore1599.yaml", null, options); + assertNotNull(swaggerParseResult.getOpenAPI()); + OpenAPI openAPI = swaggerParseResult.getOpenAPI(); + assertTrue(openAPI.getComponents().getSchemas().size() == 5); + assertNull(openAPI.getComponents().getSchemas().get("pet_category")); + assertNull(openAPI.getComponents().getSchemas().get("pet_body")); + assertNull(((Schema)openAPI.getComponents().getSchemas().get("Pet").getProperties().get("category")).get$ref()); + } + @Test public void testNPE_1685() { OpenAPIParser openAPIParser = new OpenAPIParser(); diff --git a/modules/swagger-parser/src/test/resources/petStore1599.yaml b/modules/swagger-parser/src/test/resources/petStore1599.yaml new file mode 100644 index 0000000000..a1493400d3 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/petStore1599.yaml @@ -0,0 +1,199 @@ +swagger: '2.0' +info: + description: 'This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.' + version: 1.0.0 + title: Swagger Petstore + termsOfService: 'http://swagger.io/terms/' + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: 'http://www.apache.org/licenses/LICENSE-2.0.html' +host: petstore.swagger.io +basePath: /v2 +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: 'http://swagger.io' + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user + externalDocs: + description: Find out more about our store + url: 'http://swagger.io' +schemes: + - https + - http +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + consumes: + - application/json + - application/xml + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: Pet object that needs to be added to the store + required: true + schema: + $ref: '#/definitions/Pet' + responses: + '200': + description: Succesful request + '405': + description: Invalid input + schema: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + consumes: + - application/json + - application/xml + produces: + - application/xml + - application/json + parameters: + - in: body + name: body + description: Pet object that needs to be added to the store + required: true + schema: + $ref: '#/definitions/Pet' + responses: + '200': + description: Succesful request + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + - api_key: [] +securityDefinitions: + petstore_auth: + type: oauth2 + authorizationUrl: 'https://petstore.swagger.io/oauth/authorize' + flow: implicit + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header +definitions: + Category: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Category + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/definitions/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/definitions/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: Pet + example: + id: 1000 + category: + id: 1000000 + name: category1 + name: Toby + photoUrls: + - www + - xxx + tags: + - id: 999 + name: puppy + - id: 888 + name: brown + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' \ No newline at end of file From 694e4bf44cd6fbae9f5c30b33a611a7ecc7f5df1 Mon Sep 17 00:00:00 2001 From: gracekarina Date: Thu, 26 May 2022 15:49:49 -0500 Subject: [PATCH 2/3] remove empty line --- .../java/io/swagger/v3/parser/util/InlineModelResolverTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java index 40c7d8be93..5dd8b46462 100644 --- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java @@ -835,7 +835,6 @@ public void resolveInlineArrayRequestBody() throws Exception { .requestBody(new RequestBody() .content(new Content().addMediaType("*/*",new MediaType() .schema(arraySchema)))))); - new InlineModelResolver().flatten(openAPI); RequestBody body = openAPI.getPaths().get("/hello").getGet().getRequestBody(); From 1aed230a43e50847300e14c9b6ff5a9e75350b56 Mon Sep 17 00:00:00 2001 From: gracekarina Date: Fri, 27 May 2022 19:27:05 -0500 Subject: [PATCH 3/3] fix for unexpected null values when flatten --- .../v3/parser/util/InlineModelResolver.java | 20 +- .../parser/util/InlineModelResolverTest.java | 21 ++- .../test/resources/unexpectedNullValues.yaml | 176 ++++++++++++++++++ 3 files changed, 207 insertions(+), 10 deletions(-) create mode 100644 modules/swagger-parser-v3/src/test/resources/unexpectedNullValues.yaml diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/InlineModelResolver.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/InlineModelResolver.java index 683ed41a49..a5882fcca4 100644 --- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/InlineModelResolver.java +++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/InlineModelResolver.java @@ -401,7 +401,9 @@ private void fixStringModel(Schema m) { String example = m.getExample().toString(); if (example.substring(0, 1).equals("\"") && example.substring(example.length() - 1).equals("\"")) { - m.setExample(example.substring(1, example.length() - 1)); + if(example != null || (example == null && m.getExampleSetFlag())) { + m.setExample(example.substring(1, example.length() - 1)); + } } } } @@ -629,7 +631,9 @@ public Schema modelFromProperty(ArraySchema object, @SuppressWarnings("unused") if (inner instanceof ObjectSchema) { ArraySchema model = new ArraySchema(); model.setDescription(description); - model.setExample(example); + if(example != null || (example == null && object.getExampleSetFlag())) { + model.setExample(example); + } model.setItems(object.getItems()); return model; } @@ -656,7 +660,9 @@ public Schema createModelFromProperty(Schema schema, String path) { ComposedSchema composedModel = (ComposedSchema) schema; composedModel.setDescription(description); - composedModel.setExample(example); + if(example != null || (example == null && schema.getExampleSetFlag())) { + composedModel.setExample(example); + } composedModel.setName(name); composedModel.setXml(xml); composedModel.setType(schema.getType()); @@ -672,7 +678,9 @@ public Schema createModelFromProperty(Schema schema, String path) { model.setDeprecated(schema.getDeprecated()); model.setDiscriminator(schema.getDiscriminator()); model.setEnum(schema.getEnum()); - model.setExample(example); + if(example != null || (example == null && schema.getExampleSetFlag())) { + model.setExample(example); + } model.setExclusiveMaximum(schema.getExclusiveMaximum()); model.setExclusiveMinimum(schema.getExclusiveMinimum()); model.setFormat(schema.getFormat()); @@ -718,7 +726,9 @@ public Schema modelFromProperty(Schema object, @SuppressWarnings("unused") Strin } ArraySchema model = new ArraySchema(); model.setDescription(description); - model.setExample(example); + if(example != null || (example == null && object.getExampleSetFlag())) { + model.setExample(example); + } if (object.getAdditionalProperties() != null && !(object.getAdditionalProperties() instanceof Boolean)) { model.setItems((Schema) object.getAdditionalProperties()); } diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java index 5dd8b46462..fe4a0df674 100644 --- a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/util/InlineModelResolverTest.java @@ -2,11 +2,6 @@ -import static org.testng.AssertJUnit.assertEquals; -import static org.testng.AssertJUnit.assertNotNull; -import static org.testng.AssertJUnit.assertNull; -import static org.testng.AssertJUnit.assertTrue; - import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; @@ -35,10 +30,26 @@ import io.swagger.v3.parser.OpenAPIV3Parser; import io.swagger.v3.parser.core.models.ParseOptions; +import static org.testng.AssertJUnit.*; + @SuppressWarnings({"static-method", "rawtypes"}) public class InlineModelResolverTest { @Test + public void testIssueUnexpectedNullValues() throws Exception { + ParseOptions options = new ParseOptions(); + options.setResolve(true); + options.setFlatten(true); + OpenAPI openAPI = new OpenAPIV3Parser().read("unexpectedNullValues.yaml", null, options); + assertNotNull(openAPI); + assertNull(openAPI.getComponents().getSchemas().get("DatasetDetail_schema").getExample()); + assertFalse(openAPI.getComponents().getSchemas().get("DatasetDetail_schema").getExampleSetFlag()); + //example set to null explicitly in the definition + assertNull(openAPI.getComponents().getSchemas().get("verify_datasets").getExample()); + assertTrue(openAPI.getComponents().getSchemas().get("verify_datasets").getExampleSetFlag()); + } + + @Test public void testIssue1018() throws Exception { ParseOptions options = new ParseOptions(); options.setFlatten(true); diff --git a/modules/swagger-parser-v3/src/test/resources/unexpectedNullValues.yaml b/modules/swagger-parser-v3/src/test/resources/unexpectedNullValues.yaml new file mode 100644 index 0000000000..3f23252105 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/resources/unexpectedNullValues.yaml @@ -0,0 +1,176 @@ +openapi: 3.0.3 + +info: + title: Unexpected null value at schema + version: 0.0.16 + description: | + - Unexpected null value at schema when the flatten option is set and an inline object property is flatten. + +servers: + - url: https://api.abc.com/v1 + description: null values + +paths: + /dataset: + get: + summary: | + sample for exposing issue + tags: + - dataset + responses: + 200: + description: OK - Returns metadata information for the specified dataset + content: + application/json: + schema: + $ref: '#/components/schemas/DatasetDetail' + examples: + 123: + value: + id: 123 + version: 1 + country: CHN + provider: DataZoo + description: DataZoo China National ID + + default: + description: Returns an array of errors + content: + application/json: + schema: + $ref: '#/components/schemas/Errors' + /verify: + post: + summary: | + Performs a verification. Depending on the success or failure of the datasets you will receive an HTTP response indicating if all succeeded (200), if some had problems (200) or all had problems + tags: + - verify + requestBody: + description: The verification details + required: true + content: + application/json: + schema: + type: object + properties: + datasets: + type: array + uniqueItems: true + items: + type: object + properties: + id: + description: The dataset id + type: integer + format: int32 + example: 123 + version: + description: The dataset version number + type: integer + format: int32 + example: 1 + required: + - id + - version + example: null + minItems: 1 + required: + - datasets + - subject + examples: + 123: + value: + datasets: + - id: 123 + version: 1 + credentials: + userName: username + password: password + subject: + person: + firstName: Joe + lastNames: + - Bloggs2 + identity: + documents: + - documentType: countryId + documentNumber: '37020319611025031X' + responses: + 200: + description: OK - Returns metadata information for the specified dataset + content: + application/json: + schema: + $ref: '#/components/schemas/DatasetDetail' + examples: + 123: + value: + id: 123 + version: 1 + country: CHN + provider: DataZoo + description: DataZoo China National ID + + default: + description: Returns an array of errors + content: + application/json: + schema: + $ref: '#/components/schemas/Errors' + +components: + schemas: + Dataset: + description: The high level details of a Dataset that is available within a country + type: object + properties: + id: + description: The dataset id + type: integer + format: int32 + example: 123 + version: + description: The dataset version number + type: integer + format: int32 + example: 1 + country: + description: The 3 digit ISO country code + type: string + maxLength: 3 + example: CHN + provider: + description: The dataset provider name + type: string + maxLength: 999 + example: DataZoo + description: + description: The dataset description + type: string + maxLength: 999 + example: DataZoo China National ID + required: + - id + - version + + Errors: + description: An array of errors + type: object + required: + - errors + + DatasetDetail: + description: The high level details of a Dataset that is available within a country + type: object + allOf: + - $ref: '#/components/schemas/Dataset' + properties: + schema: + type: object + properties: + request: + type: string + required: + - request + required: + - schema \ No newline at end of file