Repository: swagger-api/swagger-parser
Pull request #1647 (2022-02-02 22:31:10)

OpenAPIDeserializer: Report an error when parameter "content" definition is invalid
Fixes issue #1646


Patch:
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 dfc8faf0..33dfc418 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
@@ -1684,8 +1684,26 @@ public class OpenAPIDeserializer {
 
 		ObjectNode contentNode = getObject("content", obj, false, location, result);
 		if (contentNode != null) {
-			parameter.setContent(getContent(contentNode, String.format("%s.%s", location, "content"), result));
-		}
+            Content content = getContent(contentNode, String.format("%s.%s", location, "content"), result);
+            if(content.size() == 0) {
+                result.unsupported(location,"content with no media type",contentNode);
+                result.invalid();
+            }
+            else if(content.size() > 1) {
+                result.unsupported(location,"content with multiple media types",contentNode);
+                result.invalid();
+            }
+            else if(parameter.getSchema() != null) {
+                result.unsupported(location,"content when schema defined",contentNode);
+                result.invalid();
+            }
+            else {
+                parameter.setContent(content);
+            }
+		}
+        else if(parameter.getSchema() == null) {
+            result.missing(location,"content");
+        }
 
 		Map<String, Object> extensions = getExtensions(obj);
 		if (extensions != null && extensions.size() > 0) {


Test patch:
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 7d52415f..23869b30 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
@@ -1270,6 +1270,161 @@ public class OpenAPIDeserializerTest {
         assertEquals(result.getMessages().get(0), "attribute paths.'/realize/{param}'(post).parameters.[param].style is not of type `StyleEnum`");
     }
 
+    @Test
+    public void testParamContent() {
+        String json =
+            "{"
+            + "  \"openapi\": \"3.0.0\","
+            + "  \"info\": {"
+            + "    \"title\": \"Operations\","
+            + "    \"version\": \"0.0.0\""
+            + "  },"
+            + "  \"paths\": {"
+            + "    \"/operations\": {"
+            + "      \"post\": {"
+            + "        \"parameters\": ["
+            + "          {"
+            + "            \"name\": \"param0\","
+            + "            \"in\": \"query\","
+            + "            \"content\": {"
+            + "            }"
+            + "          },"
+            + "          {"
+            + "            \"name\": \"param1\","
+            + "            \"in\": \"query\","
+            + "            \"content\": {"
+            + "              \"text/plain\": {"
+            + "              }"
+            + "            }"
+            + "          },"
+            + "          {"
+            + "            \"name\": \"param2\","
+            + "            \"in\": \"query\","
+            + "            \"content\": {"
+            + "              \"text/plain\": {"
+            + "              },"
+            + "              \"application/json\": {"
+            + "                \"schema\": {"
+            + "                  \"type\": \"object\""
+            + "                }"
+            + "              }"
+            + "            }"
+            + "          }"
+            + "        ],"
+            + "        \"responses\": {"
+            + "          \"default\": {"
+            + "            \"description\": \"None\""
+            + "          }"
+            + "        }"
+            + "      }"
+            + "    }"
+            + "  }"
+            + "}"
+            ;
+        OpenAPIV3Parser parser = new OpenAPIV3Parser();
+        SwaggerParseResult result = parser.readContents(json, null, null);
+        Operation post = result.getOpenAPI().getPaths().get( "/operations").getPost();
+
+        Parameter param0 =
+            post.getParameters().stream()
+            .filter( p -> "param0".equals( p.getName()))
+            .findFirst()
+            .orElseThrow( () -> new IllegalStateException( "Can't find parameter=param0"));
+        assertEquals
+            (result.getMessages().contains( "attribute paths.'/operations'(post).parameters.[param0].content with no media type is unsupported"),
+             true,
+             "No media types error reported");
+        assertEquals( param0.getContent(), null, "Empty content");
+
+        Parameter param1 =
+            post.getParameters().stream()
+            .filter( p -> "param1".equals( p.getName()))
+            .findFirst()
+            .orElseThrow( () -> new IllegalStateException( "Can't find parameter=param1"));
+        assertEquals( param1.getContent().size(), 1, "Valid content size");
+
+        Parameter param2 =
+            post.getParameters().stream()
+            .filter( p -> "param2".equals( p.getName()))
+            .findFirst()
+            .orElseThrow( () -> new IllegalStateException( "Can't find parameter=param2"));
+        assertEquals
+            (result.getMessages().contains( "attribute paths.'/operations'(post).parameters.[param2].content with multiple media types is unsupported"),
+             true,
+             "Multiple media types error reported");
+        assertEquals( param2.getContent(), null, "Content with multiple media types");
+
+        assertEquals( result.getMessages().size(), 2, "Messages");
+    }
+
+    @Test
+    public void testParamData() {
+        String json =
+            "{"
+            + "  \"openapi\": \"3.0.0\","
+            + "  \"info\": {"
+            + "    \"title\": \"Operations\","
+            + "    \"version\": \"0.0.0\""
+            + "  },"
+            + "  \"paths\": {"
+            + "    \"/operations\": {"
+            + "      \"post\": {"
+            + "        \"parameters\": ["
+            + "          {"
+            + "            \"name\": \"param0\","
+            + "            \"in\": \"query\""
+            + "          },"
+            + "          {"
+            + "            \"name\": \"param2\","
+            + "            \"in\": \"query\","
+            + "            \"content\": {"
+            + "              \"text/plain\": {"
+            + "              }"
+            + "            },"
+            + "            \"schema\": {"
+            + "               \"type\": \"object\""
+            + "              }"
+            + "            }"
+            + "        ],"
+            + "        \"responses\": {"
+            + "          \"default\": {"
+            + "            \"description\": \"None\""
+            + "          }"
+            + "        }"
+            + "      }"
+            + "    }"
+            + "  }"
+            + "}"
+            ;
+        OpenAPIV3Parser parser = new OpenAPIV3Parser();
+        SwaggerParseResult result = parser.readContents(json, null, null);
+        Operation post = result.getOpenAPI().getPaths().get( "/operations").getPost();
+
+        Parameter param0 =
+            post.getParameters().stream()
+            .filter( p -> "param0".equals( p.getName()))
+            .findFirst()
+            .orElseThrow( () -> new IllegalStateException( "Can't find parameter=param0"));
+        assertEquals
+            (result.getMessages().contains( "attribute paths.'/operations'(post).parameters.[param0].content is missing"),
+             true,
+             "No schema or content error reported");
+        assertEquals( param0.getContent(), null, "No schema or content");
+
+        Parameter param2 =
+            post.getParameters().stream()
+            .filter( p -> "param2".equals( p.getName()))
+            .findFirst()
+            .orElseThrow( () -> new IllegalStateException( "Can't find parameter=param2"));
+        assertEquals
+            (result.getMessages().contains( "attribute paths.'/operations'(post).parameters.[param2].content when schema defined is unsupported"),
+             true,
+             "Both schema and content error reported");
+        assertEquals( param2.getContent(), null, "Content when schema defined");
+
+        assertEquals( result.getMessages().size(), 2, "Messages");
+    }
+
     @Test
     public void testDeserializeWithMessages() {
         String yaml = "openapi: '3.0.0'\n" +
@@ -2602,29 +2757,32 @@ public class OpenAPIDeserializerTest {
         final Paths paths = openAPI.getPaths();
         Assert.assertNotNull(paths);
 
-        PathItem petByStatusEndpoint = paths.get("/pet/findByStatus");
+        PathItem petByStatusEndpoint = paths.get("/pet/findByStatusContent");
         Assert.assertNotNull(petByStatusEndpoint.getGet());
         Assert.assertNotNull(petByStatusEndpoint.getGet().getParameters());
-        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().size(), 1);
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().size(), 3);
+
         Assert.assertNotNull(petByStatusEndpoint.getGet().getParameters().get(0).getContent());
-        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().size(),3);
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().size(),1);
         Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("application/json").getSchema().getType(),"array");
         Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("application/json").getExample(),null);
         Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("application/json").getExamples().get("list").getSummary(),"List of Names");
         Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("application/json").getSchema().getType(),"array");
 
-        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("application/xml").getExamples().get("list").getSummary(),"List of names");
-        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("application/xml").getExamples().get("list").getValue(),"<Users><User name='Bob'/><User name='Diane'/><User name='Mary'/><User name='Bill'/></Users>");
-
-        Assert.assertNotNull(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("application/xml").getExamples().get("empty").getSummary());
-        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("application/xml").getExamples().get("empty").getSummary(),"Empty list");
-        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("application/xml").getExamples().get("empty").getValue(),"<Users/>");
-
-
-        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("text/plain").getExamples().get("list").getSummary(),"List of names");
-        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("text/plain").getExamples().get("list").getValue(),"Bob,Diane,Mary,Bill");
-        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("text/plain").getExamples().get("empty").getSummary(),"Empty");
-        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(0).getContent().get("text/plain").getExamples().get("empty").getValue(),"");
+        Assert.assertNotNull(petByStatusEndpoint.getGet().getParameters().get(1).getContent());
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(1).getContent().size(),1);
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(1).getContent().get("application/xml").getExamples().get("list").getSummary(),"List of names");
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(1).getContent().get("application/xml").getExamples().get("list").getValue(),"<Users><User name='Bob'/><User name='Diane'/><User name='Mary'/><User name='Bill'/></Users>");
+        Assert.assertNotNull(petByStatusEndpoint.getGet().getParameters().get(1).getContent().get("application/xml").getExamples().get("empty").getSummary());
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(1).getContent().get("application/xml").getExamples().get("empty").getSummary(),"Empty list");
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(1).getContent().get("application/xml").getExamples().get("empty").getValue(),"<Users/>");
+
+        Assert.assertNotNull(petByStatusEndpoint.getGet().getParameters().get(2).getContent());
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(2).getContent().size(),1);
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(2).getContent().get("text/plain").getExamples().get("list").getSummary(),"List of names");
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(2).getContent().get("text/plain").getExamples().get("list").getValue(),"Bob,Diane,Mary,Bill");
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(2).getContent().get("text/plain").getExamples().get("empty").getSummary(),"Empty");
+        Assert.assertEquals(petByStatusEndpoint.getGet().getParameters().get(2).getContent().get("text/plain").getExamples().get("empty").getValue(),"");
 
         PathItem petEndpoint = paths.get("/pet");
         Assert.assertNotNull(petEndpoint.getPut());
@@ -2825,7 +2983,7 @@ public class OpenAPIDeserializerTest {
 
         final Paths paths = openAPI.getPaths();
         Assert.assertNotNull(paths);
-        Assert.assertEquals(paths.size(), 18);
+        Assert.assertEquals(paths.size(), 19);
 
         //parameters operation get
         PathItem petByStatusEndpoint = paths.get("/pet/findByStatus");
@@ -2849,7 +3007,7 @@ public class OpenAPIDeserializerTest {
 
         final Paths paths = openAPI.getPaths();
         Assert.assertNotNull(paths);
-        Assert.assertEquals(paths.size(), 18);
+        Assert.assertEquals(paths.size(), 19);
 
         //parameters operation get
         PathItem petByStatusEndpoint = paths.get("/pet/findByStatus");
@@ -2875,7 +3033,7 @@ public class OpenAPIDeserializerTest {
 
         final Paths paths = openAPI.getPaths();
         Assert.assertNotNull(paths);
-        Assert.assertEquals(paths.size(), 18);
+        Assert.assertEquals(paths.size(), 19);
 
         //parameters operation get
         PathItem petByStatusEndpoint = paths.get("/pet/findByTags");
@@ -2902,7 +3060,7 @@ public class OpenAPIDeserializerTest {
 
         final Paths paths = openAPI.getPaths();
         Assert.assertNotNull(paths);
-        Assert.assertEquals(paths.size(), 18);
+        Assert.assertEquals(paths.size(), 19);
 
         //parameters operation get
         PathItem producesTestEndpoint = paths.get("/producesTest");
@@ -2969,7 +3127,7 @@ public class OpenAPIDeserializerTest {
 
         final Paths paths = openAPI.getPaths();
         Assert.assertNotNull(paths);
-        Assert.assertEquals(paths.size(), 18);
+        Assert.assertEquals(paths.size(), 19);
 
 
         PathItem petRef = paths.get("/pathItemRef");
diff --git a/modules/swagger-parser-v3/src/test/resources/oas3.yaml.template b/modules/swagger-parser-v3/src/test/resources/oas3.yaml.template
index b88d04d4..8244c333 100644
--- a/modules/swagger-parser-v3/src/test/resources/oas3.yaml.template
+++ b/modules/swagger-parser-v3/src/test/resources/oas3.yaml.template
@@ -243,6 +243,90 @@ paths:
             - pending
             - sold
             default: available
+      responses:
+        '200':
+          description: successful operation
+          content:
+            application/xml:
+              schema:
+                type: array
+                items:
+                  "$ref": "#/components/schemas/Pet"
+            application/json:
+              schema:
+                type: array
+                items:
+                  "$ref": "#/components/schemas/Pet"
+        '400':
+          description: Invalid status value
+      security:
+      - petstore_auth:
+        - write:pets
+        - read:pets
+      requestBody:
+        description: pet store to add to the system
+        required: true
+        content:
+          multipart/mixed:
+            schema:
+              type: object
+              properties:
+                id:
+                  # default is text/plain
+                  type: string
+                  format: uuid
+                  "$ref": "#/components/schemas/Pet"
+                address:
+                  # default is application/json
+                  type: object
+                  properties: {}
+                historyMetadata:
+                  # need to declare XML format!
+                  description: metadata in XML format
+                  type: object
+                  properties: {}
+                profileImage:
+                  # default is application/octet-stream, need to declare an image type only!
+                  type: string
+                  format: binary
+            encoding:
+              historyMetadata:
+                # require XML Content-Type in utf-8 encoding
+                contentType: application/xml; charset=utf-8
+              profileImage:
+                # only accept png/jpeg
+                contentType: image/png, image/jpeg
+                headers:
+                  X-Rate-Limit:
+                    description: calls per hour allowed by the user
+                    schema:
+                      type: integer
+                      format: int32
+                  X-Expires-After:
+                    description: date in UTC when token expires
+                    schema:
+                      type: string
+                      format: date-time
+      callbacks:
+        mainHook:
+          '$request.body#/url':
+            post:
+              responses:
+                '200':
+                  description: webhook successfully processed operation
+  "/pet/findByStatusContent":
+    get:
+      tags:
+      - pet
+      summary: Finds Pets by status
+      description: Multiple status values can be provided with comma separated strings
+      operationId: findPetsByStatusContent
+      parameters:
+      - name: status
+        in: query
+        description: Status values that need to be considered for filter
+        required: true
+        example: "example"
         content:
           'application/json':
             schema:
@@ -274,6 +358,12 @@ paths:
                     schema:
                       type: integer
                       format: int32
+      - name: statusXml
+        in: query
+        description: Status values that need to be considered for filter
+        required: true
+        example: "example"
+        content:
           'application/xml':
             examples:
               list:
@@ -282,6 +372,12 @@ paths:
               empty:
                 summary: Empty list
                 value: "<Users/>"
+      - name: statusText
+        in: query
+        description: Status values that need to be considered for filter
+        required: true
+        example: "example"
+        content:
           'text/plain':
             examples:
               list:
diff --git a/modules/swagger-parser/src/test/resources/issue959.json b/modules/swagger-parser/src/test/resources/issue959.json
index d5220d70..f5ad745a 100644
--- a/modules/swagger-parser/src/test/resources/issue959.json
+++ b/modules/swagger-parser/src/test/resources/issue959.json
@@ -123,7 +123,10 @@
           {
             "name":"petId",
             "in":"path",
-            "required": true
+            "required": true,
+            "schema": {
+              "type": "integer"
+            }
 
           }
         ],
@@ -222,4 +225,4 @@
       }
     }
   }
-}
\ No newline at end of file
+}