Spring Boot controller - Upload Multipart and JSON to DTO
I want to upload a file inside a form to a Spring Boot API endpoint.
The UI is written in React:
export function createExpense(formData) {
return dispatch => {
axios.post(ENDPOINT,
formData,
headers: {
'Authorization': //...,
'Content-Type': 'application/json'
}
).then(({data}) => {
//...
})
.catch(({response}) => {
//...
});
};
}
_onSubmit = values => {
let formData = new FormData();
formData.append('title', values.title);
formData.append('description', values.description);
formData.append('amount', values.amount);
formData.append('image', values.image[0]);
this.props.createExpense(formData);
}
This is the java side code:
@RequestMapping(path = "/{groupId}", method = RequestMethod.POST)
public ExpenseSnippetGetDto create(@RequestBody ExpensePostDto expenseDto, @PathVariable long groupId, Principal principal, BindingResult result) throws IOException {
//..
}
But I get this exception on the Java side:
org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'multipart/form-data;boundary=----WebKitFormBoundaryapHVvBsdZYc6j4Af;charset=UTF-8' not supported
How should I resolve this issue? The similar API endpoints and JavaScript side code is already working.
Note
I've seen a solution where it suggests that the request body should have 2 attributes: one which the JSON section goes under, another for the image. I'd like to see if it is possible to have it automatically converted to DTO.
Update 1
The upload payload sent by the client should be converted to the following DTO:
public class ExpensePostDto extends ExpenseBaseDto {
private MultipartFile image;
private String description;
private List<Long> sharers;
}
So you can say it's a mix of JSON and multipart.
Solution
The solution to the problem is to use FormData
on the front-end and ModelAttribute
on the backend:
@RequestMapping(path = "/{groupId}", method = RequestMethod.POST,
consumes = {"multipart/form-data"})
public ExpenseSnippetGetDto create(@ModelAttribute ExpensePostDto expenseDto, @PathVariable long groupId, Principal principal) throws IOException {
//...
}
and on the front-end, get rid of Content-Type
as it should be determined by the browser itself, and use FormData
(standard JavaScript). That should solve the problem.
Solution 1:
Yes, you can simply do it via wrapper class.
1) Create a Class
to hold form data:
public class FormWrapper {
private MultipartFile image;
private String title;
private String description;
}
2) Create an HTML form
for submitting data:
<form method="POST" enctype="multipart/form-data" id="fileUploadForm" action="link">
<input type="text" name="title"/><br/>
<input type="text" name="description"/><br/><br/>
<input type="file" name="image"/><br/><br/>
<input type="submit" value="Submit" id="btnSubmit"/>
</form>
3) Create a method to receive form's text
data and multipart
file:
@PostMapping("/api/upload/multi/model")
public ResponseEntity<?> multiUploadFileModel(@ModelAttribute FormWrapper model) {
try {
// Save as you want as per requiremens
saveUploadedFile(model.getImage());
formRepo.save(mode.getTitle(), model.getDescription());
} catch (IOException e) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
return new ResponseEntity("Successfully uploaded!", HttpStatus.OK);
}
4) Method to save file
:
private void saveUploadedFile(MultipartFile file) throws IOException {
if (!file.isEmpty()) {
byte[] bytes = file.getBytes();
Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());
Files.write(path, bytes);
}
}
Solution 2:
I had created a similar thing using pure JS and Spring Boot.
Here is the Repo.
I'm sending an User
object as JSON
and a File
as part of the multipart/form-data
request.
The relevant snippets are below
The Controller
code
@RestController
public class FileUploadController {
@RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = { "multipart/form-data" })
public void upload(@RequestPart("user") @Valid User user,
@RequestPart("file") @Valid @NotNull @NotBlank MultipartFile file) {
System.out.println(user);
System.out.println("Uploaded File: ");
System.out.println("Name : " + file.getName());
System.out.println("Type : " + file.getContentType());
System.out.println("Name : " + file.getOriginalFilename());
System.out.println("Size : " + file.getSize());
}
static class User {
@NotNull
String firstName;
@NotNull
String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return "User [firstName=" + firstName + ", lastName=" + lastName + "]";
}
}
}
The HTML
and JS
code
<html>
<head>
<script>
function onSubmit() {
var formData = new FormData();
formData.append("file", document.forms["userForm"].file.files[0]);
formData.append('user', new Blob([JSON.stringify({
"firstName": document.getElementById("firstName").value,
"lastName": document.getElementById("lastName").value
})], {
type: "application/json"
}));
var boundary = Math.random().toString().substr(2);
fetch('/upload', {
method: 'post',
body: formData
}).then(function (response) {
if (response.status !== 200) {
alert("There was an error!");
} else {
alert("Request successful");
}
}).catch(function (err) {
alert("There was an error!");
});;
}
</script>
</head>
<body>
<form name="userForm">
<label> File : </label>
<br/>
<input name="file" type="file">
<br/>
<label> First Name : </label>
<br/>
<input id="firstName" name="firstName" />
<br/>
<label> Last Name : </label>
<br/>
<input id="lastName" name="lastName" />
<br/>
<input type="button" value="Submit" id="submit" onclick="onSubmit(); return false;" />
</form>
</body>
</html>