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 449a9e3a6f..0c89680c45 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 @@ -59,15 +59,13 @@ import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; public class SwaggerConverter implements SwaggerParserExtension { + + private static final Set STRIPPED_EXTENSION_KEYS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("x-example", "x-examples", "x-nullable"))); + private List globalConsumes = new ArrayList<>(); private List globalProduces = new ArrayList<>(); private Components components = new Components(); @@ -654,14 +652,17 @@ public Operation convert(io.swagger.models.Operation v2Operation) { } private Map convert(Map vendorExtensions) { - if (vendorExtensions != null && vendorExtensions.size() > 0) { - vendorExtensions.entrySet().removeIf(extension -> ( - extension.getKey().equals("x-example")) || - extension.getKey().equals("x-examples") || - extension.getKey().equals("x-nullable")); + if (vendorExtensions == null || vendorExtensions.isEmpty()) { + return vendorExtensions; } - return vendorExtensions; + Map result = new LinkedHashMap<>(); + for (Map.Entry entry : vendorExtensions.entrySet()) { + if (!STRIPPED_EXTENSION_KEYS.contains(entry.getKey())) { + result.put(entry.getKey(), entry.getValue()); + } + } + return result; } private Schema convertFormDataToSchema(io.swagger.models.parameters.Parameter formParam) { 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 b185a7c944..6dfc974cba 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 @@ -100,6 +100,7 @@ public class V2ConverterTest { private static final String ISSUE_1767_YAML = "issue-1767.yaml"; private static final String ISSUE_1796_YAML = "issue-1796.yaml"; + private static final String ISSUE_2269_YAML = "issue-2269.yaml"; private static final String API_BATCH_PATH = "/api/batch/"; private static final String PETS_PATH = "/pets"; @@ -928,6 +929,42 @@ public void testConvertFormDataAsObjectSchema() throws Exception { } + @Test + public void testIssue2269SharedResponseNullable() { + SwaggerConverter converter = new SwaggerConverter(); + ParseOptions parseOptions = new ParseOptions(); + parseOptions.setResolve(true); + parseOptions.setResolveFully(true); + SwaggerParseResult result = converter.readLocation( + "src/test/resources/" + ISSUE_2269_YAML, null, parseOptions); + assertNotNull(result); + OpenAPI oas = result.getOpenAPI(); + assertNotNull(oas); + + for (String endpoint : Arrays.asList("/endpoint1", "/endpoint2")) { + Map properties = oas.getPaths().get(endpoint).getGet() + .getResponses().get("200").getContent().values().iterator().next() + .getSchema().getProperties(); + assertNotNull(properties, endpoint + " schema properties must not be null"); + + Schema nullableField = properties.get("nullable_field"); + assertNotNull(nullableField, endpoint + " must have nullable_field"); + assertEquals(nullableField.getNullable(), Boolean.TRUE, + endpoint + " nullable_field must have nullable: true"); + assertNotNull(nullableField.getExtensions(), endpoint + " nullable_field must retain extensions"); + assertEquals(nullableField.getExtensions().get("x-internal-note"), "abc", + endpoint + " nullable_field must retain x-internal-note extension"); + + Schema nonNullableField = properties.get("non_nullable_field"); + assertNotNull(nonNullableField, endpoint + " must have non_nullable_field"); + assertEquals(nonNullableField.getNullable(), Boolean.FALSE, + endpoint + " non_nullable_field must have nullable: false"); + assertNotNull(nonNullableField.getExtensions(), endpoint + " non_nullable_field must retain extensions"); + assertEquals(nonNullableField.getExtensions().get("x-internal-note"), "def", + endpoint + " non_nullable_field must retain x-internal-note extension"); + } + } + /** * A clone (almost) of {@link OpenAPIV3Parser#readContents(String, List, ParseOptions)}. */ diff --git a/modules/swagger-parser-v2-converter/src/test/resources/issue-2269.yaml b/modules/swagger-parser-v2-converter/src/test/resources/issue-2269.yaml new file mode 100644 index 0000000000..6aaee0f965 --- /dev/null +++ b/modules/swagger-parser-v2-converter/src/test/resources/issue-2269.yaml @@ -0,0 +1,41 @@ +swagger: '2.0' +info: + version: 1.0.0 + title: Issue 2269 - x-nullable preservation for shared $ref responses + +responses: + SharedResponse: + description: A shared response with multiple fields using x-nullable + schema: + type: object + properties: + data: + type: string + description: Required field + nullable_field: + type: integer + x-nullable: true + x-internal-note: abc + non_nullable_field: + type: integer + x-nullable: false + x-internal-note: def + required: + - data + +paths: + /endpoint1: + get: + operationId: endpoint1 + summary: First endpoint using shared response + responses: + '200': + $ref: '#/responses/SharedResponse' + + /endpoint2: + get: + operationId: endpoint2 + summary: Second endpoint using shared response + responses: + '200': + $ref: '#/responses/SharedResponse' 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 ada569d28d..f8b3f3f480 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 @@ -686,6 +686,32 @@ public void testIssue1552() throws Exception { assertNotNull(schema.getProperties().get("foo")); } + @Test(description = "Issue 2269: preserve x-nullable in shared responses for swagger 2.0 specs") + public void testSwagger2SharedResponseNullable() { + ParseOptions options = new ParseOptions(); + options.setResolve(true); + options.setResolveFully(true); + + SwaggerParseResult result = new OpenAPIParser().readLocation("issue2269.yaml", null, options); + + assertNotNull(result); + assertNotNull(result.getOpenAPI()); + OpenAPI openAPI = result.getOpenAPI(); + + Schema endpoint1Field = (Schema) openAPI.getPaths().get("/endpoint1").getGet() + .getResponses().get("200").getContent().values().iterator().next() + .getSchema().getProperties().get("optional_field"); + assertNotNull(endpoint1Field, "Endpoint 1 should have optional_field"); + assertEquals(endpoint1Field.getNullable(), Boolean.TRUE, "Endpoint 1 optional_field should be nullable"); + + Schema endpoint2Field = (Schema) openAPI.getPaths().get("/endpoint2").getGet() + .getResponses().get("200").getContent().values().iterator().next() + .getSchema().getProperties().get("optional_field"); + assertNotNull(endpoint2Field, "Endpoint 2 should have optional_field"); + assertEquals(endpoint2Field.getNullable(), Boolean.TRUE, "Endpoint 2 optional_field should be nullable"); + + } + @org.testng.annotations.Test(description = "convert response schema") public void testIssue1552AdditionalProps() throws Exception { ParseOptions options = new ParseOptions(); diff --git a/modules/swagger-parser/src/test/resources/issue2269.yaml b/modules/swagger-parser/src/test/resources/issue2269.yaml new file mode 100644 index 0000000000..19116c0f10 --- /dev/null +++ b/modules/swagger-parser/src/test/resources/issue2269.yaml @@ -0,0 +1,38 @@ +swagger: '2.0' +info: + version: 1.0.0 + title: Swagger 2.0 Shared Response Test + description: Test case for verifying x-nullable is preserved when multiple endpoints share the same response definition + +responses: + SharedResponse: + description: A shared response with nullable field + schema: + type: object + properties: + data: + type: string + description: Required field + optional_field: + type: integer + description: Optional nullable field + x-nullable: true + required: + - data + +paths: + /endpoint1: + get: + operationId: endpoint1 + summary: First endpoint using shared response + responses: + '200': + $ref: '#/responses/SharedResponse' + + /endpoint2: + get: + operationId: endpoint2 + summary: Second endpoint using shared response + responses: + '200': + $ref: '#/responses/SharedResponse'