Repository: springdoc/springdoc-openapi (2025-08-05T17:45:21Z)

Issue:
Non-primitive types for RequestPart don't show when consuming multipart/form-data
Springdoc fails to generate proper schema information for @RequestPart parameters with generic types (e.g., `Map<String, Any>`) in multipart/form-data endpoints. The parameter disappears entirely from Swagger UI, making it impossible for API consumers to understand the expected input format.

This issue is specific to multipart form data endpoints using @RequestPart with generic types - the same generic types work perfectly fine with @RequestBody in regular JSON endpoints.

Note that SpringBoot has absolutely no problem dealing with the `Map<String,Any>` - only springdoc does.

**To Reproduce**

```kt
package springfuckup

import io.swagger.v3.oas.annotations.*
import io.swagger.v3.oas.annotations.media.*
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.*

@SpringBootApplication
@RestController
@Tag(name = "Hello World", description = "Simple Hello World endpoint")
class App {
    @Operation(description = "Passing a string works")
    @PostMapping("/string-works", consumes = ["multipart/form-data"])
    fun string(
        @Parameter(
            description = "A JSON object for file mapping",
            required = true,
        )
        @RequestPart fileMapping: String,
    ): String {
        return "Received file mapping with length ${fileMapping.length}"
    }

    @Operation(description = "Passing a map doesn't work")
    @PostMapping("/map-works", consumes = ["multipart/form-data"])
    fun map(
        @Parameter(
            description = "A JSON object for file mapping",
            required = true,
        )
        @RequestPart fileMapping: Map<String, Any>,
    ): String {
        return "Received file mapping with ${fileMapping.size} entries"
    }
}

fun main(args: Array<String>) {
    runApplication<App>(*args)
}

fun main(args: Array<String>) {
    runApplication<App>(*args)
}
```


```kts
// build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "3.2.5"
    id("io.spring.dependency-management") version "1.1.5"
    kotlin("jvm") version "1.9.24"
    kotlin("plugin.spring") version "1.9.24"
}

group = "springfuckup"
version = "0.0.1-SNAPSHOT"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "21"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

springBoot {
    mainClass.set("springfuckup.AppKt")
}
```

```sh
./gradlew bootRun
```

then open `http://localhost:8080/swagger-ui/index.html#/Hello%20World/string` and observe that the `/map-works` endpoint doesn't show the required request body.

**Expected behavior**

Required parameter shows for `/map-works` in request body just as it does for `/string-works`

**Screenshots**

See how the parameter only shows for primitive "string" `@RequestPart` but not for `Map<String,Any>`

<img width="1469" height="999" alt="Image" src="https://github.com/user-attachments/assets/38ccb561-12e9-4f84-a23d-a5b56dea05e4" />

**Additional context**

Full MRE is available here: https://github.com/corneliusroemer/springdocbug


Discussion:
Hi @corneliusroemer,

Since springdoc generates an openapi specification that is then visualized by the separate project [swagger-ui](https://github.com/swagger-api/swagger-ui), it would be easier to understand the issue if the specification was shared rather than the swagger-ui view.

The reason for asking for the specification is also since I would like to know what expected type that you would expect this property to have (I.e., how would you expect the specification to look if the property was to be properly included)? Would it just be a field with `"additionalProperties": false` or a more limiting 
```
"additionalProperties": {
  "type": "string"
}
```
(if the map were to be `Map<String, String>`).

Or would it be something else?
I looked at how a RequestBody with a Map is represented, which was 
```
additionalProperties": {
  "type": "string"
}
```
So I have made a fix that changes so that that is how it will be represented in the specification.
Hi @Mattias-Sehlstedt, apologies for not replying earlier, I was on vacation. Thanks for fixing this, I'll try it out as soon as a release is cut!



Patch:
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/ParameterInfo.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/ParameterInfo.java
index 11e7acc0f..93098ce6d 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/ParameterInfo.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/models/ParameterInfo.java
@@ -85,7 +85,7 @@ public class ParameterInfo {
 	private String paramType;
 
 	/**
-	 * if the paramater type is RequestPart
+	 * if the parameter type is RequestPart
 	 */
 	private boolean requestPart;
 
diff --git a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java
index 681a61570..401be359e 100644
--- a/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java
+++ b/springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/AbstractRequestService.java
@@ -471,6 +471,8 @@ public boolean isParamToIgnore(MethodParameter parameter) {
 			return false;
 		if (isRequestBodyWithMapType(parameter))
 			return false;
+		if (isRequestPartWithMapType(parameter))
+			return false;
 		return isRequestTypeToIgnore(parameter.getParameterType());
 	}
 
@@ -510,6 +512,23 @@ private boolean isRequestBodyWithMapType(MethodParameter parameter) {
 		return Map.class.isAssignableFrom(parameterType);
 	}
 
+	/**
+	 * Is request part with map type
+	 *
+	 * @param parameter the parameter
+	 * @return the boolean
+	 */
+	private boolean isRequestPartWithMapType(MethodParameter parameter) {
+		// Check for @RequestPart annotation
+		org.springframework.web.bind.annotation.RequestPart requestPart = parameter.getParameterAnnotation(org.springframework.web.bind.annotation.RequestPart.class);
+		if (requestPart == null) {
+			return false;
+		}
+		// Check if the parameter type is assignable to Map
+		Class<?> parameterType = parameter.getParameterType();
+		return Map.class.isAssignableFrom(parameterType);
+	}
+
 	/**
 	 * Sets params.
 	 *


Test patch:
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app52/HelloController.java b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app52/HelloController.java
index 550b17a97..bbdb8e549 100644
--- a/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app52/HelloController.java
+++ b/springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v30/app52/HelloController.java
@@ -24,42 +24,52 @@
 
 package test.org.springdoc.api.v30.app52;
 
-import java.util.List;
-
 import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestHeader;
-import org.springframework.web.bind.annotation.RequestPart;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.List;
+import java.util.Map;
+
 @RestController
 public class HelloController {
 
-	@PostMapping(value = "/test1/{username}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
-	public String createTest1(@PathVariable String username, @RequestPart("test") MyTestDto test,
-			@RequestPart("image") MultipartFile imageFile) {
-		return null;
-	}
+    @PostMapping(value = "/test1/{username}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public String createTest1(
+            @PathVariable String username,
+            @RequestPart("test") MyTestDto test,
+            @RequestPart("image") MultipartFile imageFile) {
+        return null;
+    }
+
+    @PostMapping(value = "/test2/{username}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public String createTest2(
+            @PathVariable String username,
+            @RequestPart("image") MultipartFile imageFile,
+            @RequestPart("test") MyTestDto test,
+            @RequestHeader("My-Header") String workspaceId) {
+        return null;
+    }
 
-	@PostMapping(value = "/test2/{username}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
-	public String createTest2(@PathVariable String username, @RequestPart("image") MultipartFile imageFile,
-			@RequestPart("test") MyTestDto test, @RequestHeader("My-Header") String workspaceId) {
-		return null;
-	}
+    @PostMapping(value = "/test3", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public String createTest3(
+            @RequestPart("test") MyTestDto test,
+            @RequestPart("doc") List<MultipartFile> multipartFiles) {
+        return null;
+    }
 
-	@PostMapping(value = "/test3", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
-	public String createTest3(@RequestPart("test") MyTestDto test,
-			@RequestPart("doc") List<MultipartFile> multipartFiles) {
-		return null;
-	}
+    @PostMapping(value = "/test4", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+    public String createTest4(
+            @RequestPart List<MultipartFile> multipartFiles,
+            @RequestPart Map<String, String> map) {
+        return null;
+    }
 
-	class MyTestDto {
-		public String object1;
+    class MyTestDto {
+        public String object1;
 
-		public String object2;
+        public String object2;
 
-		public String object3;
-	}
+        public String object3;
+    }
 }
\ No newline at end of file
diff --git a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app52.json b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app52.json
index 351bef514..09f4f2d88 100644
--- a/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app52.json
+++ b/springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.0.1/app52.json
@@ -11,6 +11,54 @@
     }
   ],
   "paths": {
+    "/test4": {
+      "post": {
+        "tags": [
+          "hello-controller"
+        ],
+        "operationId": "createTest4",
+        "requestBody": {
+          "content": {
+            "multipart/form-data": {
+              "schema": {
+                "required": [
+                  "map",
+                  "multipartFiles"
+                ],
+                "type": "object",
+                "properties": {
+                  "multipartFiles": {
+                    "type": "array",
+                    "items": {
+                      "type": "string",
+                      "format": "binary"
+                    }
+                  },
+                  "map": {
+                    "type": "object",
+                    "additionalProperties" : {
+                      "type": "string"
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/test3": {
       "post": {
         "tags": [