[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();
}
};