OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
21.62k stars 6.53k forks source link

[BUG][JAVA] Generated ApiClient does not respect 'encoding' object for multipart request bodies, uses hard-coded application/octet-stream for all file form-data #16215

Open stavvy-gkillough opened 1 year ago

stavvy-gkillough commented 1 year ago

Description

The OpenAPI Specification v3 (3.0.0) describes an encoding object for multipart request bodies. The openapi-generator does not honor this in its Java client generation, and instead uses a hard-coded application/octet-stream. I've taken a peek at the generator templates for Java and determined this is indeed hard-coded and could be set dynamically.

Source: v3 encoding-object

This is most important for file-upload use-cases, but should ideally be supported for all form-data parameters.

openapi-generator version

v6.6.0 of the generator (via the openapi-generator-cli)

OpenAPI declaration file content or url

post:
  operationId: uploadPdf
  requestBody:
    content:
      multipart/form-data:
        schema:
          type: object
          required:
            - pdfFile
          properties:
            pdfFile:
              type: string
              format: binary
        encoding:
          pdfFile:
            contentType: application/pdf

Note: I've stripped my form data down to just the file, but in my actual use-case there are several parameters other than pdfFile.

Steps to reproduce

Using the yaml provided above, observe a request from a generated client does not set the Content-Type header of the individual form data parameters based on the encoding object, but instead uses the hard-coded application/octet-stream. Looking at the generated ApiClient.java file's serialize method definition reveals the hard-coded value as well.

Related issues/PRs

Suggest a fix

For all the Java clients, I suggest passing in a Map<String, String> with the key being the form param name, and the value being the value found in the encoding object in the yaml. Then use Map.getOrDefault(param.getKey(), MediaType.APPLICATION_OCTET_STREAM_TYPE) rather than the hard-coded octet stream type.

The functional code fix: ApiClient.mustache in the serialize(...) method:

        } else if (param.getValue() instanceof File) {
          File file = (File) param.getValue();
          String fileContentType = formParamContentTypes.getOrDefault(param.getKey(), MediaType.APPLICATION_OCTET_STREAM_TYPE)
          mp.bodyPart(new FileDataBodyPart(param.getKey(), file, fileContentType));
        }

The following methods would need to pass an additional Map<String, String> to support this change:

The api.mustache template would need to add something like this:

    Map<String, Object> localVarFormParams = new HashMap<String, Object>();
    Map<String, String> localVarFormParamContentTypes = new HashMap<String, String>();

Note: Changing the serialize, invokeAPI, and getAPIResponse methods would result in a breaking change if ApiClient is considered part of the public API of the generated client.

vali-m commented 1 year ago

I also have this problem:

"content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "body"
                ],
                "properties": {
                  "body": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/MyRequest"
                      },
                      ...
                    ]
                  },
                  ...
                }
              },
              "encoding": {
                "body": {
                  "contentType": "application/json"
                }
              }
            }
          }

In the generated API Client Class, no content type is specified, so "text/plain" is used instead of "application/json".

I also can't think of a workaround without adding the generated files to my git repository, which I really want to avoid doing. I tried it on both 6.6.0 and 7.0.1.

ZBSTooling commented 1 year ago

Same problem here:

paths:
  /fileupload:
    post:
      description: https://swagger.io/docs/specification/describing-request-body/multipart-requests/
      operationId: uploadFiles
      tags:
        - FileUpload
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                address:
                  $ref: '#/components/schemas/address'
                files:
                  type: array
                  items:
                    type: string
                    format: binary
            encoding:
              address:
                contentType: application/json
      responses:
        202:
          description: OK

With openapi-generator 7.0.1 the following native client code is generated:

private HttpRequest.Builder uploadFilesRequestBuilder(Address address, List<File> files) throws ApiException {
        HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder();
        String localVarPath = "/fileupload";
        localVarRequestBuilder.uri(URI.create(this.memberVarBaseUri + localVarPath));
        localVarRequestBuilder.header("Accept", "application/json");
        MultipartEntityBuilder multiPartBuilder = MultipartEntityBuilder.create();
        boolean hasFiles = false;
        multiPartBuilder.addTextBody("address", address.toString());

        for(int i = 0; i < files.size(); ++i) {
            multiPartBuilder.addBinaryBody("files", (File)files.get(i));
            hasFiles = true;
        }

        HttpEntity entity = multiPartBuilder.build();

The encoding part at the address object is missing. It should looks like something like this:

    private HttpRequest.Builder uploadFilesRequestBuilder(Address address, List<File> files) throws ApiException
    {
        HttpRequest.Builder localVarRequestBuilder = HttpRequest.newBuilder();
        String localVarPath = "/fileupload";
        localVarRequestBuilder.uri(URI.create(this.memberVarBaseUri + localVarPath));
        localVarRequestBuilder.header("Accept", "application/json");
        MultipartEntityBuilder multiPartBuilder = MultipartEntityBuilder.create();
        boolean hasFiles = false;
        try {
            multiPartBuilder.addTextBody("address", memberVarObjectMapper.writeValueAsString(address), ContentType.APPLICATION_JSON);
        }
        catch (JsonProcessingException e) {
            throw new ApiException(e);
        }

        for (int i = 0; i < files.size(); ++i) {
            multiPartBuilder.addBinaryBody("files", (File) files.get(i));
            hasFiles = true;
        }
vali-m commented 1 year ago

@ZBSTooling I found a workaround. By using the default library (okhttp-gson), object parts sent in a multipart request have the content type ‘application/json’.

I don’t understand why the native library does not set the content type json for object parts…