102

I want to post a file with some JSON data using Spring MVC. So I've developed a rest service as

@RequestMapping(value = "/servicegenerator/wsdl", method = RequestMethod.POST,consumes = { "multipart/mixed", "multipart/form-data" })
@ResponseBody
public String generateWSDLService(@RequestPart("meta-data") WSDLInfo wsdlInfo,@RequestPart("file") MultipartFile file) throws WSDLException, IOException,
        JAXBException, ParserConfigurationException, SAXException, TransformerException {
    return handleWSDL(wsdlInfo,file);
}

When I send a request from the rest client with content-Type = multipart/form-data or multipart/mixed, I get the next exception: org.springframework.web.multipart.support.MissingServletRequestPartException

Can anyone help me in solving this issue?

Can I use @RequestPart to send both Multipart and JSON to a server?

nikiforovpizza
  • 487
  • 1
  • 7
  • 13
Sunil Kumar
  • 5,477
  • 4
  • 31
  • 38

6 Answers6

250

This is how I implemented Spring MVC Multipart Request with JSON Data.

Multipart Request with JSON Data (also called Mixed Multipart):

Based on RESTful service in Spring 4.0.2 Release, HTTP request with the first part as XML or JSON formatted data and the second part as a file can be achieved with @RequestPart. Below is the sample implementation.

Java Snippet:

Rest service in Controller will have mixed @RequestPart and MultipartFile to serve such Multipart + JSON request.

@RequestMapping(value = "/executesampleservice", method = RequestMethod.POST,
    consumes = {"multipart/form-data"})
@ResponseBody
public boolean executeSampleService(
        @RequestPart("properties") @Valid ConnectionProperties properties,
        @RequestPart("file") @Valid @NotNull @NotBlank MultipartFile file) {
    return projectService.executeSampleService(properties, file);
}

Front End (JavaScript) Snippet:

  1. Create a FormData object.

  2. Append the file to the FormData object using one of the below steps.

    1. If the file has been uploaded using an input element of type "file", then append it to the FormData object. formData.append("file", document.forms[formName].file.files[0]);
    2. Directly append the file to the FormData object. formData.append("file", myFile, "myfile.txt"); OR formData.append("file", myBob, "myfile.txt");
  3. Create a blob with the stringified JSON data and append it to the FormData object. This causes the Content-type of the second part in the multipart request to be "application/json" instead of the file type.

  4. Send the request to the server.

  5. Request Details:
    Content-Type: undefined. This causes the browser to set the Content-Type to multipart/form-data and fill the boundary correctly. Manually setting Content-Type to multipart/form-data will fail to fill in the boundary parameter of the request.

Javascript Code:

formData = new FormData();

formData.append("file", document.forms[formName].file.files[0]);
formData.append('properties', new Blob([JSON.stringify({
                "name": "root",
                "password": "root"                    
            })], {
                type: "application/json"
            }));

Request Details:

method: "POST",
headers: {
         "Content-Type": undefined
  },
data: formData

Request Payload:

Accept:application/json, text/plain, */*
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryEBoJzS3HQ4PgE1QB

------WebKitFormBoundaryvijcWI2ZrZQ8xEBN
Content-Disposition: form-data; name="file"; filename="myfile.txt"
Content-Type: application/txt


------WebKitFormBoundaryvijcWI2ZrZQ8xEBN
Content-Disposition: form-data; name="properties"; filename="blob"
Content-Type: application/json


------WebKitFormBoundaryvijcWI2ZrZQ8xEBN--
Ash
  • 672
  • 6
  • 21
Sunil Kumar
  • 5,477
  • 4
  • 31
  • 38
  • Hi, what is ConnectionProperties, just a POJO? – Chuanshi Liu Nov 09 '15 at 09:46
  • Hi Liu, Yes Connection properties is just a POJO class.Our aim is to take some pojo object and multipart in the rest request. – Sunil Kumar Mar 03 '16 at 05:24
  • 1
    Nicely done. I had to use `processData: false, contentType: false` with `JQuery $ajax()` – sura2k May 14 '16 at 16:56
  • If you ever looking to do a curl to construct this request payload, refer this answer which has [multipart curl command](http://stackoverflow.com/a/37874979/150371) – raksja Jun 17 '16 at 06:56
  • 2
    @SunilKumar, If i need to give file upload as optional..?with form Data.How can i do this. Because if image is not selected am getting `Required request part file is not present` – Hema May 24 '17 at 09:36
  • It can be off topic question. Do you have RAML example for request with parts? – Vadym Perepeliak Sep 01 '17 at 06:41
  • 1
    For me the "new Blob([JSON.stringify(...)]" part did it.. I had everything else in place. Thx. – Ostati May 15 '18 at 20:49
  • 1
    With angular 5, the line `...headers: { "Content-Type": undefined }` Caused issues with the HttpClient from Angular/Common. Instead, I just did not define the Content Type and it worked! – KeaganFouche May 28 '18 at 09:51
  • Using this method I'm getting an error because Spring is expecting a class, not an interface. Any ideas? – Kenny Worden Jul 20 '18 at 04:59
  • What about uploading Array of "connectionProperties and file" in one request? – Muhammed Ozdogan Jul 31 '18 at 12:34
  • What happens if we change consumes to 'multipart/mixed' ? – Praveen Shendge Oct 06 '18 at 19:57
  • 9
    @SunilKumar did you have to specify Converter for ConnectionProperties ? If I use a pojo like shown above for ConnectionProperties I get ... HttpMediaTypeNotSupportedException: Content type 'application/octet-stream' not supported If I change the POJO to String it works. So its not clear how the conversion to the POJO is happening ? – user2412398 Jan 04 '19 at 04:29
  • 1
    Just to make this clear: The `@NotBlank` annotation on the MultipartFile method parameter won't actually check if the file is empty. It is still possible to upload documents with 0 bytes. – sn42 Feb 12 '19 at 08:23
15

This must work!

client (angular):

$scope.saveForm = function () {
      var formData = new FormData();
      var file = $scope.myFile;
      var json = $scope.myJson;
      formData.append("file", file);
      formData.append("ad",JSON.stringify(json));//important: convert to JSON!
      var req = {
        url: '/upload',
        method: 'POST',
        headers: {'Content-Type': undefined},
        data: formData,
        transformRequest: function (data, headersGetterFunction) {
          return data;
        }
      };

Backend-Spring Boot:

@RequestMapping(value = "/upload", method = RequestMethod.POST)
    public @ResponseBody
    Advertisement storeAd(@RequestPart("ad") String adString, @RequestPart("file") MultipartFile file) throws IOException {

        Advertisement jsonAd = new ObjectMapper().readValue(adString, Advertisement.class);
//do whatever you want with your file and jsonAd
mohi
  • 1,093
  • 2
  • 16
  • 21
2

You can also use the next way a list List<MultipartFile> and @RequestPart("myObj") as parameters in your method inside a @RestController


    @PostMapping()
    @ResponseStatus(HttpStatus.CREATED)
    public String create(@RequestPart("file") List<MultipartFile> files, @RequestPart("myObj") MyJsonDTOClass myObj) throws GeneralSecurityException, IOException {
        // your code

    }

and in the axios side with a bit of react:

            const jsonStr = JSON.stringify(myJsonObj);
            const blob = new Blob([jsonStr], {
                type: 'application/json'
            });

            let formData = new FormData();
            formData.append("myObj",blob );
            formData.append("file", this.state.fileForm); // check your control 
            let url = `your url`            
            let method = `post`
            let headers =
                {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json'
                };
            }

            axios({
                method,
                url,
                data: formData,
                headers
            }).then(res => {
                console.log(res);
                console.log(res.data);
            });

JPRLCol
  • 749
  • 11
  • 28
1

We've seen in our projects that a post request with JSON and files is creating a lot of confusion between the frontend and backend developers, leading to unnecessary wastage of time.

Here's a better approach: convert file bytes array to Base64 string and send it in the JSON.

public Class UserDTO {
    private String firstName;
    private String lastName;
    private FileDTO profilePic; 
}

public class FileDTO {
    private String base64;
    // just base64 string is enough. If you want, send additional details
    private String name;
    private String type;
    private String lastModified;
}

@PostMapping("/user")
public String saveUser(@RequestBody UserDTO user) {
    byte[] fileBytes = Base64Utils.decodeFromString(user.getProfilePic().getBase64());
    ....
}

JS code to convert file to base64 string:

var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function () {

  const userDTO = {
    firstName: "John",
    lastName: "Wick",
    profilePic: {
      base64: reader.result,
      name: file.name,
      lastModified: file.lastModified,
      type: file.type
    }
  }
  
  // post userDTO
};
reader.onerror = function (error) {
  console.log('Error: ', error);
};
Drunken Daddy
  • 7,326
  • 14
  • 70
  • 104
0

As documentation says:

Raised when the part of a "multipart/form-data" request identified by its name cannot be found.

This may be because the request is not a multipart/form-data either because the part is not present in the request, or because the web application is not configured correctly for processing multipart requests -- e.g. no MultipartResolver.

Vaelyr
  • 2,841
  • 2
  • 21
  • 34
0

For Angular2+ users. Try to send JSON payload in a mixed part request as below.

formData.append("jsonPayload", new Blob([JSON.stringify(json)], {
  type: "application/json"
}));

Given below complete function.

submit() {
    const formData = new FormData();
    formData.append('file', this.myForm.get('fileSource').value);
    var json = {
        "key":"value"
    };

  formData.append("jsonPayload", new Blob([JSON.stringify(json)], {
    type: "application/json"
  }));

  this.http.post('http://localhost:8080/api/mixed-part-endpoint', formData)
  .subscribe(res => {
    console.log(res);
    alert('Uploaded Successfully.');
  })
}
Hardik
  • 115
  • 1
  • 8