Spring Boot controller - Upload Multipart and JSON to DTO

JavascriptJavaSpring Boot

Javascript Problem Overview


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.

Javascript Solutions


Solution 1 - Javascript

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:






  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 - Javascript

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>

Solution 3 - Javascript

I had a similar use case where I had some JSON data and image upload (Think of it as a user trying to register with a personal details and profile image).

Referring to @Stephan and @GSSwain answer I came up with a solution with Spring Boot and AngularJs.

Below is a snapshot of my code. Hope it helps someone.

    var url = "https://abcd.com/upload";
    var config = {
   		headers : {
   			'Content-Type': undefined
   	    }
        
    }
    var data = {
        name: $scope.name,
        email: $scope.email
    }
    $scope.fd.append("obj", new Blob([JSON.stringify(data)], {
                type: "application/json"
            }));
	
    $http.post(
        url, $scope.fd,config
    )
        .then(function (response) {
            console.log("success", response)
            // This function handles success

        }, function (response) {
            console.log("error", response)
            // this function handles error

        });

And SpringBoot controller:

@RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = {   "multipart/form-data" })
@ResponseBody
public boolean uploadImage(@RequestPart("obj") YourDTO dto, @RequestPart("file") MultipartFile file) {
	// your logic
	return true;
}

Solution 4 - Javascript

I built my most recent file upload application in AngularJS and SpringBoot which are similar enough in syntax to help you here.

My client side request handler:

uploadFile=function(fileData){
	var formData=new FormData();
	formData.append('file',fileData);
	return $http({
		method: 'POST',
		url: '/api/uploadFile',
		data: formData,
		headers:{
			'Content-Type':undefined,
			'Accept':'application/json'
		}
	});
};

One thing to note is Angular automatically sets the multipart mime type and boundary on the 'Content-Type' header value for me. Yours may not, in which case you need to set it yourself.

My application expects a JSON response from the server, thus the 'Accept' header.

You are passing in the FormData object yourself, so you need to make sure that your form is setting the File to whatever attribute you map to on your Controller. In my case it is mapped to the 'file' parameter on the FormData object.

My controller endpoints look like this:

@POST
@RequestMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file) 
{
    if (file.isEmpty()) {
        return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
    } else {
        //...
    }
}

You can add as many other @RequestParam as you'd like, including your DTO that represents the rest of the form, just make sure its structured that way as a child of the FormData object.

The key take-away here is that each @RequestParam is an attribute on the FormData object body payload on the multipart request.

If I were to modify my code to accommodate your data, it would look something like this:

uploadFile=function(fileData, otherData){
	var formData=new FormData();
	formData.append('file',fileData);
    formData.append('expenseDto',otherData);
	return $http({
		method: 'POST',
		url: '/api/uploadFile',
		data: formData,
		headers:{
			'Content-Type':undefined,
			'Accept':'application/json'
		}
	});
};

Then your controller endpoint would look like this:

@POST
@RequestMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file, @RequestParam("expenseDto") ExpensePostDto expenseDto)
{
    if (file.isEmpty()) {
        return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
    } else {
        //...
    }
}

Solution 5 - Javascript

@RequestMapping(value = { "/test" }, method = { RequestMethod.POST })
@ResponseBody
public String create(@RequestParam("file") MultipartFile file, @RequestParam String description, @RequestParam ArrayList<Long> sharers) throws Exception {
    ExpensePostDto expensePostDto = new ExpensePostDto(file, description, sharers);
    // do your thing
    return "test";
}

This seems to be the easiest way out here, other ways could be to add your own messageConverter.

Solution 6 - Javascript

Add the consumer type to your request mapping .it should work fine.

@POST
@RequestMapping("/upload")
public ResponseEntity<Object> upload(@RequestParam("file") MultipartFile file,consumes = "multipart/form-data") 
{
    if (file.isEmpty()) {
        return new ResponseEntity<Object>(HttpStatus.BAD_REQUEST);
    } else {
        //...
    }
}

Solution 7 - Javascript

You have to tell spring you're consuming multipart/form-data by adding consumes = "multipart/form-data" to the RequestMapping annotation. Also remove the RequestBody annotation from the expenseDto parameter.

@RequestMapping(path = "/{groupId}", consumes = "multipart/form-data", method = RequestMethod.POST)
public ExpenseSnippetGetDto create(ExpensePostDto expenseDto, 
   @PathVariable long groupId, Principal principal, BindingResult result) 
   throws IOException {
   //..
}

With the posted ExpensePostDto the title in the request is ignored.

Edit

You'll also need to change the content type to multipart/form-data. Sounds like that's the default for post based on some other answers. Just to be safe, I would specify it:

'Content-Type': 'multipart/form-data'

Solution 8 - Javascript

Remove this from the react front end:

 'Content-Type': 'application/json'

Modify the Java side controller:

   @PostMapping("/{groupId}")
   public Expense create(@RequestParam("image") MultipartFile image,  @RequestParam("amount") double amount, @RequestParam("description") String description, @RequestParam("title") String title) throws IOException {
         //storageService.store(file); ....
          //String imagePath = path.to.stored.image;
         return new Expense(amount, title, description, imagePath);
 }
 

This can be written better but tried keeping it as close to your original code as much as I could. I hope it helps.

Solution 9 - Javascript

In my case the problem was the DTO class having public fields and not getter/setter methods

public class MyDTO {
    public MultipartFile[] files;
    public String user;
    public String text;
}

The DTO class MUST have getters/setters, otherwise it won't work.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionArianView Question on Stackoverflow
Solution 1 - JavascriptUsamaAmjadView Answer on Stackoverflow
Solution 2 - JavascriptGSSwainView Answer on Stackoverflow
Solution 3 - JavascriptvisrahaneView Answer on Stackoverflow
Solution 4 - JavascriptStephanView Answer on Stackoverflow
Solution 5 - JavascriptRaghvendra GargView Answer on Stackoverflow
Solution 6 - Javascriptthis_is_om_vmView Answer on Stackoverflow
Solution 7 - JavascriptShaneCoderView Answer on Stackoverflow
Solution 8 - JavascriptFemi NefaView Answer on Stackoverflow
Solution 9 - JavascriptlainatnaviView Answer on Stackoverflow