swagger-codegen icon indicating copy to clipboard operation
swagger-codegen copied to clipboard

[JAVA] Serialization of parts of multipart request bodies

Open mathieutho opened this issue 6 years ago • 9 comments

Description

I am using the Swagger Codegen to generate a JAVA client to request a Spring Boot API. This API provides a POST web service which takes a JSON parameter "dto" as a part of multipart/form-data. I am using the "encoding" attribute in the swagger declaration but the client does not seem to use it. The parameter is not serialized to JSON but is sent as the string representation of the class.

Swagger-codegen version

swagger-codegen-cli-3.0.5-20190130.172841-36.jar

Swagger declaration file content or url

Open API v3.0.2

"requestBody": {
  "content": {
    "multipart/form-data": {
      "schema": {
        "type": "object",
        "properties": {
          "dto": {
            "$ref": "#/components/schemas/PushConfigurationDto"
          }
        }
      },
      "encoding": {
        "dto": {
          "contentType": "application/json"
        }
      }
    }
  }
}

This case corresponds to the "Complex Serialization in Form Data" example described into this documentation.

Command line used for generation

java -jar swagger-codegen-cli-3.0.5-20190130.172841-36.jar generate -i http://192.168.5.159:8080/openApi -l java -o ./java_api_client

Suggest a fix/enhancement

Is there a solution to make the client serializing multiparts other than String?

mathieutho avatar Jan 31 '19 15:01 mathieutho

In class ApiClient.java when building the request (method buildRequest),

} else if ("multipart/form-data".equals(contentType)) {
    reqBody = buildRequestBodyMultipart(formParams);  <-- multipart
    ...
} else {
    reqBody = serialize(body, contentType);  

buildRequestBodyMultipart is called but in this case, Java objects are not correctly transformed. Only File objects are taken into account, the others are converted into a string, when they should be transformed into json (Content type is "application/json" and not "text/plain").

Without multipart this operation was correctly done in method serialize (taking into account the Content-type)

public RequestBody serialize(final Object obj, final String contentType) throws ApiException {
    if (obj instanceof byte[]) {
        // Binary (byte array) body parameter support.
        return RequestBody.create(MediaType.parse(contentType), (byte[]) obj);
    } else if (obj instanceof File) {
        // File body parameter support.
        return RequestBody.create(MediaType.parse(contentType), (File) obj);
    } else if (isJsonMime(contentType)) {
        String content;
        if (obj != null) {
            content = this.json.serialize(obj);  <--- TADAM

Any idea to solve that problem ?

abarbee avatar Feb 01 '19 15:02 abarbee

meanwhile we modify the class ApiClient with this patch

public RequestBody buildRequestBodyMultipart(final Map<String, Object> formParams) throws ApiException {
    MultipartBuilder mpBuilder = new MultipartBuilder().type(MultipartBuilder.FORM);
    for (Entry<String, Object> param : formParams.entrySet()) {
        if (param.getValue() instanceof File) {
            File file = (File) param.getValue();
            Headers partHeaders = Headers.of("Content-Disposition", "form-data; name=\"" + param.getKey() + "\"; filename=\"" + file.getName() + "\"");
            MediaType mediaType = MediaType.parse(guessContentTypeFromFile(file));
            mpBuilder.addPart(partHeaders, RequestBody.create(mediaType, file));
        } else if (!(param.getValue() instanceof String)) {
            Headers partHeaders = Headers.of("Content-Disposition", "form-data; name=\"" + param.getKey() + "\"");
           	mpBuilder.addPart(partHeaders, serialize(param.getValue(), "application/json"));
        } else {
            Headers partHeaders = Headers.of("Content-Disposition", "form-data; name=\"" + param.getKey() + "\"");
                mpBuilder.addPart(partHeaders, RequestBody.create(null, parameterToString(param.getValue())));
            
        }
    }
    return mpBuilder.build();
}

abarbee avatar Feb 04 '19 09:02 abarbee

I think there is the same error in other langages. For example csharp. in generated classe < name >Api.cs each objects parameters are transformed into a string instead of a json.

< name >Api.cs if (dto != null) localVarFormParams.Add("dto", this.Configuration.ApiClient.ParameterToString(dto)); // form parameter

ApiClient.cs public string ParameterToString(object obj) { if (obj is DateTime) ... else if (obj is DateTimeOffset) ... else if (obj is IList) { ... else return Convert.ToString (obj); } json case is not processed,

abarbee avatar Feb 05 '19 08:02 abarbee

Has there been any activity on this issue? We running up against the same thing with generated Java client code. Our workaround is similar to the one suggested by @abarbee but instead uses the Accept HTTP header to determine whether an Object should be serialized.

ApiClient.cs

    public Request buildRequest(String path, String method, List<Pair> queryParams, List<Pair> collectionQueryParams, Object body, Map<String, String> headerParams, Map<String, Object> formParams, String[] authNames, ProgressRequestBody.ProgressRequestListener progressRequestListener) throws ApiException {
       ...      
        String accepts = (String) headerParams.get("Accept");
        // ensuring a default content type
        if (accepts == null) {
        	accepts = "application/json";
        }

        RequestBody reqBody;
        if (!HttpMethod.permitsRequestBody(method)) {
            reqBody = null;
        } else if ("application/x-www-form-urlencoded".equals(contentType)) {
            reqBody = buildRequestBodyFormEncoding(formParams);
        } else if ("multipart/form-data".equals(contentType)) {
            reqBody = buildRequestBodyMultipart(formParams, accepts);
        } else if (body == null) {
            if ("DELETE".equals(method)) {
                // allow calling DELETE without sending a request body
                reqBody = null;
            } else {
                // use an empty request body (for POST, PUT and PATCH)
                reqBody = RequestBody.create(MediaType.parse(contentType), "");
            }
        } else {
            reqBody = serialize(body, contentType);
        }
      ...
        return request;
    }
  public RequestBody buildRequestBodyMultipart(Map<String, Object> formParams, String accepts) {
        MultipartBuilder mpBuilder = new MultipartBuilder().type(MultipartBuilder.FORM);
        for (Entry<String, Object> param : formParams.entrySet()) {
            if (param.getValue() instanceof File) {
                File file = (File) param.getValue();
                Headers partHeaders = Headers.of("Content-Disposition", "form-data; name=\"" + param.getKey() + "\"; filename=\"" + file.getName() + "\"");
                MediaType mediaType = MediaType.parse(guessContentTypeFromFile(file));
                mpBuilder.addPart(partHeaders, RequestBody.create(mediaType, file));
            } else {
                Headers partHeaders = Headers.of("Content-Disposition", "form-data; name=\"" + param.getKey() + "\"");
                if(isJsonMime(accepts)) {
                	MediaType mediaType = MediaType.parse("application/json");
                	mpBuilder.addPart(partHeaders, RequestBody.create(mediaType, json.serialize(param.getValue())));
                }
                else {
                	mpBuilder.addPart(partHeaders, RequestBody.create(null, parameterToString(param.getValue())));
                }
            }
        }
        return mpBuilder.build();
    }

The optimal solution would be to update the code generation process such that building a request can directly respect the encdoding/contentType assigned to each property of the multipart message and only rely on this guessing/inspection process if no encoding is specified.

apanetta88 avatar Apr 30 '19 14:04 apanetta88

I do not know, how to overcome this issue normal way, have to set this field to binary which is realy ugly.

kilork avatar Jun 20 '20 16:06 kilork

I've got the same issue in Javascript.

rcky avatar Sep 14 '20 12:09 rcky

I have fixed it without the need of coding just adjusting the pom.xml. To generate the client using swagger-codegen-maven-plugin, just add this code inside de configuration:

<typeMappings>
<typeMapping>multipartFile=java.io.File</typeMapping>
</typeMappings>

With this elements we are adding a new type "multipartFile" that could be used in the openapi file and we are telling swagger to change it into java.io.File. Next step will be adjust the type of this parameters and use the created multipartFile. Watch the openapi example:

  /example/file-upload:
    post:
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                image:
                  type: multipartFile

Now when we execute swagger-codegen, the Controller that will be generated will have a java.io.File input parameter and the ApiClient will turn it into a MultipartFile that will match the server side.

If we want to build the server side, we just need to adjust the typeMapping. Assuming we are using springboot, the mapping must match pultipartFile into org.springframework.web.multipart.MultipartFile.

victorsempere avatar Sep 23 '21 06:09 victorsempere

Resurrecting an older (but open!) thread, but seeing as it's pretty much exactly the same...

I am hitting this same issue for Java. I am also hitting the inverse/something very similar in reverse. Just like how abarbee describes that his POJO is being serialized to String instead of JSON upon serialization (i.e. the declared encoding is not respected) I am finding that the declared encoding is also not respected upon de-serialization either (i.e. this means it treats my JSON form data, which has a local Content-Type header of application/json, as a string instead of json, which in turn means it fails to de-serialize it into my DTO).

This feels like it is impossible to go contract-first where you have a multipart/form-data where one part has JSON content. Is this really the case?

ncgisudo avatar Apr 13 '22 18:04 ncgisudo

This seems to be quire similar to an issue I'm running into with typescript-angular. Where DTOs are not being serialized at all when in a multipart request body. (backend is aspnet)

If doing something like this

public async Task<IActionResult> AddNewFormTemplateVersionAsync(
        [FromRoute] Guid formTemplateId,
        [FromForm] FormTemplateVersionDataDto formTemplateVersionData,
        [FromForm] IFormFile pdfFile)
{}

I would expect that formTemplateVersionData would be added to the formParams and JSON.stringified, however in this case the dto object is broken up into individual properies as is the file. And nothing is stringified.

If instead this is done

public async Task<IActionResult> AddNewFormTemplateVersionAsync(
        [FromRoute] Guid formTemplateId,
        [FromBody] FormTemplateVersionDataDto formTemplateVersionData,
        [FromForm] IFormFile pdfFile)
{}

only the dto object shows up in the generated call, the file is ignored.

If [FromBody] is left out then the dto is assumed to be a query parameter when it isn't and it isn't stringified anyway. The file is however included as the formParams and passed in the body.

My current minor fix for this is to modify the mustache template so that in the case of formParams and a bodyParam, the bodyParam is stringified and added to the formParams, and the order of operations in the http request is flipped so that formParams take precedence over the bodyParam, as body will be included in formParams if both exist.

korydondzila avatar Feb 17 '23 21:02 korydondzila