Spring Boot starter that enables automatic binding of nested multipart/form-data requests to complex DTO structures. No more manual HttpServletRequest parsing — just annotate your controller parameter with @NestedMultipart.
- Deeply nested DTOs — bind files at any depth (
a.b.c.file) - Collections —
List,Set, and arrays of DTOs, each with their own files - Map —
Map<String, MultipartFile>for dynamic file fields - Direct file lists —
List<MultipartFile>andMultipartFile[] Optional<MultipartFile>— files that may or may not be present- JSON part merging — combine JSON bodies with file parts in the same request
- Custom prefixes — override the parameter name prefix via
@NestedMultipart("data") - Works alongside
@RequestParam— handle mixed parameters naturally - Zero configuration — auto-registers via Spring Boot auto-configuration
- Java 17+
- Spring Boot 3.2+
- Spring Web MVC (not WebFlux)
repositories {
mavenCentral()
}
dependencies {
implementation 'io.github.howtis:nested-multipart-spring-boot-starter:1.0.0'
}<dependency>
<groupId>io.github.howtis</groupId>
<artifactId>nested-multipart-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>// DTOs
public class Attachment {
private MultipartFile file;
private String description;
// getters and setters
}
public class PostForm {
private String title;
private Attachment attachment;
// getters and setters
}
// Controller
@PostMapping("/posts")
public ResponseEntity<?> create(@NestedMultipart PostForm form) {
// form.getTitle() -> "My Post"
// form.getAttachment().getFile() -> the uploaded file
// form.getAttachment().getDescription() -> "My description"
}Send the request with multipart parts:
form.title: My Post
form.attachment.description: My description
form.attachment.file: [binary data]
DTOs can be nested arbitrarily deep. File parts use dot-notation to match the DTO structure:
public class C {
private MultipartFile file;
}
public class B {
private C c;
}
public class A {
private B b;
}
@PostMapping("/deep")
public ResponseEntity<?> deep(@NestedMultipart A a) {
// a.getB().getC().getFile() -> the uploaded file
}Request part name: a.b.c.file
public class Item {
private MultipartFile file;
private String description;
}
public class ItemListForm {
private List<Item> items;
}
@PostMapping("/items")
public ResponseEntity<?> createItems(@NestedMultipart ItemListForm form) {
// form.getItems().get(0).getFile() -> first file
// form.getItems().get(1).getFile() -> second file
}Request parts:
form.items[0].description: First item
form.items[0].file: [binary data]
form.items[1].description: Second item
form.items[1].file: [binary data]
Discontinuous indices (e.g., [0] and [2]) are supported — missing indices are filled with null.
Same as List<Item> but using java.util.Set<Item>:
public class SetItemForm {
private Set<SetItem> items;
}public class ArrayAttachmentForm {
private Attachment[] attachments;
}
@PostMapping("/attachments")
public ResponseEntity<?> createAttachments(@NestedMultipart ArrayAttachmentForm form) {
// form.getAttachments()[0].getFile() -> first file
}When a field is List<MultipartFile> or MultipartFile[], files are injected directly without nested DTO wrappers:
public class MultiFileForm {
private List<MultipartFile> images;
}
@PostMapping("/images")
public ResponseEntity<?> uploadImages(@NestedMultipart MultiFileForm form) {
// form.getImages().get(0) -> first image
// form.getImages().get(1) -> second image
}Send multiple files with the same part name:
form.images: [binary data 1]
form.images: [binary data 2]
public class MapForm {
private Map<String, MultipartFile> documents;
private String title;
}
@PostMapping("/documents")
public ResponseEntity<?> uploadDocuments(@NestedMultipart MapForm form) {
// form.getDocuments().get("contract") -> contract file
// form.getDocuments().get("id") -> id file
}Request parts:
form.title: Documents
form.documents[contract]: [binary data]
form.documents[id]: [binary data]
public class OptionalFileForm {
private Optional<MultipartFile> avatar;
private Optional<MultipartFile> thumbnail;
}
@PostMapping("/profile")
public ResponseEntity<?> uploadProfile(@NestedMultipart OptionalFileForm form) {
// form.getAvatar().isPresent() -> true if avatar was sent
// form.getThumbnail().isPresent() -> true if thumbnail was sent
}Missing files result in Optional.empty().
Send structured data as a JSON part alongside file parts. The JSON fields are merged into the DTO using Jackson's readerForUpdating:
public class ArticleForm {
private String title;
private String tags;
private MultipartFile thumb;
}
@PostMapping("/article")
public ResponseEntity<?> createArticle(@NestedMultipart ArticleForm form) {
// form.getTitle() -> "Hello World"
// form.getTags() -> "java,spring"
// form.getThumb() -> the uploaded thumbnail
}Request parts:
form: {"title":"Hello World","tags":"java,spring"} (Content-Type: application/json)
form.thumb: [binary data]
If a field appears in both the JSON part and a text parameter, a conflict error is raised.
Override the parameter name prefix via the annotation value:
@PostMapping("/submit")
public ResponseEntity<?> submit(@NestedMultipart("data") PostForm form) {
// Parts are expected under "data." prefix
// data.title, data.attachment.file, ...
}@NestedMultipart works alongside standard @RequestParam:
@PostMapping("/submit")
public ResponseEntity<?> submit(
@NestedMultipart PostForm form,
@RequestParam("tag") String tag) {
// Both form and tag are bound correctly
}The binding happens in three passes:
flowchart TD
A[multipart/form-data Request] --> B[Pass 1: DataBinder]
B --> C[Pass 1.5: JSON Merge]
C --> D[Pass 2: File Injection]
B -->|Binds text fields| E["form.title, form.items[0].description"]
C -->|Merges JSON body| F["form: {'title':'...','tags':'...'}"]
D -->|Injects MultipartFile| G[form.attachment.file, form.images]
- Pass 1 —
ServletRequestDataBinderbinds text/primitive fields (strips the parameter name prefix) - Pass 1.5 — JSON parts (
Content-Type: application/json) are deserialized and merged via Jackson. Conflicts with Pass 1 fields are rejected. - Pass 2 —
MultipartFileFieldInjectorrecursively traverses the DTO tree via reflection, matching file parts by dot-path and injectingMultipartFileinstances
- DTOs must have a no-arg constructor
- Fields must have getters and setters following JavaBeans conventions
- Add
-parameterscompiler flag to avoid specifying the prefix in the annotation (Gradle: configured automatically; Maven: set<parameters>true</parameters>inmaven-compiler-plugin)
No configuration required. The starter auto-registers through Spring Boot's AutoConfiguration.imports mechanism.
# If you want to disable it (not typical):
spring.autoconfigure.exclude=io.github.howtis.nestedmultipart.NestedMultipartAutoConfiguration- Only works with Spring Web MVC (Servlet-based), not WebFlux
- DTOs must have a no-arg constructor
Mapfields with non-MultipartFilevalues (e.g.,Map<String, String>) are bound by Pass 1 only and use standard Spring binding notation