Skip to content
GitLab
Projects Groups Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Sign in / Register
  • O openapi-generator
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
  • Issues 3,476
    • Issues 3,476
    • List
    • Boards
    • Service Desk
    • Milestones
  • Merge requests 402
    • Merge requests 402
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Infrastructure Registry
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • OpenAPI Tools
  • openapi-generator
  • Issues
  • #11289
Closed
Open
Issue created Jan 12, 2022 by Administrator@rootContributor2 of 6 checklist items completed2/6 checklist items

[BUG][Java] Complex values are serialized as the string [object Object] in multipart/form-data (Java variant of #7658)

Created by: rami-hanninen

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator (example)?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

Dear OpenAPITools maintainers.

I would like to report an OpenAPITool 'java' generator specific variant for an earlier known issue that has already been fixed for 'typescript-fetch' generator. All the symptoms, observations, and the fix itself already described in issue 'https://github.com/OpenAPITools/openapi-generator/issues/7658' hold, except that the bug still applies to generated 'java' clients, instead of generated clients of 'typescript-fetch' type (and presumable to most of the other language variants).

A brief summary of the issue is, that all complex (Java) model data objects that appear in any 'multipart/form-data' requests are currently NOT serialized with JSON, but instead with each corresponding object own auto-generated 'toString()' method. The strings these methods generate are completely inappropriate for any HTTP request use, as they are formatted to be human-readable debugging aids, instead of formal representations of the corresponding object state, like a JSON serialization would be.

A more detailed description of the issue is, that the automatically generated ApiClient class buildRequestBodyMultipart method fails to take into account that the objects that provide content for the individual parts of the request may need JSON encoding to properly represent their state. Specifically, current implementation classifies 'multipart/form-data' parts as either instances of the 'File' class, or something else, and all objects falling into the category of "something else" are serialized with 'parameterToString(Object param)' method.

That method in turn further classifies objects as various Date class instances, Collection instances, and "something else". All objects of "something else" type, and all members of Collections, are then serialized with String.valueOf(Object), which ultimately uses each object own 'toString()' method to produce the object "serialized" value.

Reliance to objects own 'toString()' for object state serialization is just wrong, specifically when the data objects themselves are automatically generated model data objects that have 'toString()' implementations that produce human readable debugging dumps of the object state.

As far as I can see, the same issue probably applies to ALL OpenAPITools client language generators, except for those for which it may have already been explicitly fixed (like 'typescript-fetch').

I also think that this issue dates back to the original Swagger utility that OpenAPITools was originally forked from.

openapi-generator version

The issue applies to the (currently) latest OpenAPITools version 5.3.1, and to all previous versions (up to and including all versions of the the original Swagger utility that OpenApiTools is a fork of). This also implies that this is not a regression, but a shortcoming that has always been there.

This issue has probably stayed hidden this long because OpenAPI versions before 3.0.0 did not support multipart/form-data at all. Furthermore, requests of this kind also are probably needed rather infrequently, as the only use cases that really need them are file uploads with a bundle of complex parameters.

OpenAPI declaration file content or url

Any OpenAPI declaration that declares a multipart/form-data operation like the pseudo-declaration below will trigger this bug when corresponding 'java' client code is generated.

  /end-point-path:
    post:
      summary: A multipart/form-data request.
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                complexItem:
                  $ref: '#/components/schemas/ComplexItem'
                fileItem:
                  type: string
                  format: binary
    ComplexItem:
      type: object
      properties:
        property1:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/AnotherComplexItem'
    AnotherComplexItem:
      type: object
      properties:
        property2:
          type: object
Generation Details
  <plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <executions>
      <execution>
        <goals>
          <goal>generate</goal>
        </goals>
        <configuration>
          <configHelp>false</configHelp>
          <configOptions>
            <apiPackage>sample.package</apiPackage>
            <artifactId>sample.artifact.id</artifactId>
            <invokerPackage>sample.invoker.package</invokerPackage>
            <modelPackage>sample.model.package</modelPackage>
            <sourceFolder>src/gen/java/main</sourceFolder>
          </configOptions>
          <generatorName>java</generatorName>
          <inputSpec>sample-swagger.yaml</inputSpec>
          <verbose>false</verbose>
        </configuration>
      </execution>
    </executions>
  </plugin>
Steps to reproduce

We are using 'openapi-generator-maven-plugin' in a complex micro-service build, and I'm not going to replicate the whole setup here. Any invocation of OpenAPITools that generates code for a request involving a multipart/form-data request with complex data objects should do.

Observed/expected output

The affected 'multipart/form-data' parts in HTTP POST request bodies contain inappropriate object human-readable debugging string representation of form:

class ClassName { propertyName: propertyValue }

The expected result would be a properly serialized representation in corresponding JSON form.

Related issues/PRs

https://github.com/OpenAPITools/openapi-generator/issues/7658

Suggest a fix

We have worked around the bug for now with an anonymous ApiClient sub-class that overrides the default 'buildRequestBodyMultipart(Map<String,Object> formParams)' with the following revised implementation. It probably will not work for general cases exactly as given, but it should give an example of one possible approach. Please also see how the earlier similar issue #7658 (closed) has already been fixed.

ApiClient fixedClient = new ApiClient(someOkHttpClient) { // --- Private interface ---

  /**
   * Tests if given parameter is a complex client model instance.
   *
   * @param param parameter to test
   *
   * @return if given parameter is a complex client model instance
   *
   * @see #buildRequestBodyMultipart(Map)
   */

  private boolean isComplex(Object param)
  {
    ... our own private implementation that serves as a workaround for us, but does not work for general use cases ...
  }

  private String partToString(Object param)
  {
    if(param == null)
      return "";

    if(param instanceof Date ||
       param instanceof OffsetDateTime ||
       param instanceof LocalDate)
    {
      // Serialize to json string and remove the " enclosing characters

      String jsonStr = getJSON().serialize(param);

      return jsonStr.substring(1, jsonStr.length() - 1);
    }

    if(param instanceof Collection)
    {
      StringBuilder b = new StringBuilder();

      for(Object o : (Collection)param)
      {
        if(b.length() > 0)
          b.append(",");

        b.append(String.valueOf(o));
      }

      return b.toString();
    }

    return getJSON().serialize(param);
  }

  // --- ApiClient interface ---

  /**
   * {@inheritDoc}
   *
   * <P>
   *
   * This implementation fixes inherent shortcoming in the default
   * implementation that fails to encode complex part values with JSON.
   * Unlike the super-class implementation, if a part value is recognized
   * to be complex, as determined by {@link #isComplex(Object)}, the
   * part value is serialized with {@link #partToString(Object)}, as opposed
   * to the default {@link #parameterToString(Object)}.
   *
   * @see #isComplex(Object)
   * @see #partToString(Object)
   */

  @Override
  public RequestBody buildRequestBodyMultipart(Map<String,Object> formParams)
  {
    MultipartBody.Builder mpBuilder =
      new MultipartBody.Builder().setType(MultipartBody.FORM);

    for(Entry<String,Object> param : formParams.entrySet())
    {
      Object value = param.getValue();

      // Note: Headers.of(...) expects alternating header names and values.

      if(value instanceof File)
      {
        File file = (File)value;
        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(file, mediaType));
      }
      else if(isComplex(value))
      {
        // Note: Requests of 'multipart/form-data' type should accept part
        //       specific 'Content-Type' headers, but support for them in
        //       general is weak, and OkHttp3 actually explicitly rejects
        //       them.

        Headers partHeaders =
          Headers.of("Content-Disposition",
                     "form-data; name=\"" + param.getKey() + "\"" /*,
                     "Content-Type",
                     "application/json"*/);
        String string = partToString(value);

        mpBuilder.addPart(partHeaders,RequestBody.create(string,null));
      }
      else // Simple
      {
        Headers partHeaders =
          Headers.of("Content-Disposition",
                     "form-data; name=\"" + param.getKey() + "\"");
        String string = parameterToString(value);

        mpBuilder.addPart(partHeaders,RequestBody.create(string,null));
      }
    }

    return mpBuilder.build();
  }
};
Assignee
Assign to
Time tracking