[BUG] Do not auto-select content type as application/json if available in content types list
Bug Report Checklist
- [x] Have you provided a full/minimal spec to reproduce the issue?
- [x] Have you validated the input using an OpenAPI validator (example)?
- [ ] Have you tested with the latest master to confirm the issue still exists?
Couldn't build the project in local due to missing JDK. However, I see no changes to the code templates.
- [x] Have you searched for related issues/PRs?
- [x] What's the actual output vs expected output?
- [ ] [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description
If multiple payload content types are set, ex: json and multipart, json is always picked as default header.
My requirement: If the payload is a json payload, the server expects a file_key which was uploaded via an upload endpoint, and if it is multipart, a file is expected.
openapi-generator version
5.1.1
OpenAPI declaration file content or url
https://gist.github.com/tiholic/99cf45e7eebe24ff4fc3bc02802f4485
Generation Details
openapi-generator generate -i http://nestjs-server.localhost/docs-json/ -o ./ -g python --additional-properties generateSourceCodeOnly=true,packageName=example_api.example
Steps to reproduce
Use the schema from gist and try to call the action endpoint with a file payload open('a.txt', 'rb') and you can see that Content-Type is posted as application/json.
api_client = example.ApiClient(configuration)
api_client.action(open('a.txt', 'rb')) # errors out from server as server receives no file
Related issues/PRs
None that I can find very close, but there are PRs and issues reported that revolve around enhancing select_header_content_type function.
Suggest a fix
A Content-Type vs Open API Type map must be available to auto select content type based in the input.
For example, from this schema (see gist for full schema),
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/FileInput"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileInputAsJson"
}
}
}
}
we can create a map like this (or whatever makes sense w.r.t language)
{
"#/components/schemas/FileInput": "multipart/form-data",
"#/components/schemas/FileInputAsJson": "application/json"
}
A hack I use to get out of this situation, currently:
I create an 2 api clients, one for forcing multipart content type,
multipart_api_client = example.ApiClient(configuration)
multipart_api_client.set_default_headers('Content-Type', 'multipart/form-data')
multipart_api_client.action(open('a.txt', 'rb')) # works
and other for default
default_api_client = example.ApiClient(configuration)
I found the same problem.
What follows is what I could gather from inspecting the python templates and messing around with them as well as tweaking my API schema. Don't take this for a good answer. Verify. I'm not maintainer of this repo. I'm just starting to use it.
Big Assumption
When an endpoint accepts multiple content-types, the client code is generated from the first available option.
Example
given the order x-www-form-urlencoded, json, as in
{
"post": {
"operationId": "api_token_auth_create",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/AuthTokenRequest"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/AuthTokenRequest"
}
}
...
This code is generated
def api_token_auth_create(
self,
username: Annotated[str, Field(min_length=1, strict=True)],
password: Annotated[str, Field(min_length=1, strict=True)],
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]
],
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[AuthToken]:
Swapping them, this code is generated.
@validate_call
def api_token_auth_create(
self,
auth_token_request: AuthTokenRequest,
_request_timeout: Union[
None,
Annotated[StrictFloat, Field(gt=0)],
Tuple[
Annotated[StrictFloat, Field(gt=0)], Annotated[StrictFloat, Field(gt=0)]
],
] = None,
_request_auth: Optional[Dict[StrictStr, Any]] = None,
_content_type: Optional[StrictStr] = None,
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[AuthToken]:
Note the difference: the first arguments (username / password vs. AuthTokenRequest). This per se is not the problem.
It's just to show that code generator seems to be using the first available content-type.
consequence
There are more differences later (I'm not including them to avoid writing a book here) but in the first scenario the username and password will end up being lost (not sent at all), due to the wrong content-type selection.
I think there is an error in the selection of the request content type. If you don' specify it, then self.api_client.select_header_content_type is called, which will always favor application/json if it's available.
Workarounds:
-
swap content type options in your schema
-
Force correct content-type on a single request
This is the safest and easiest.
Every call to an operation accepts _content_type as an argument so you may change that on a per request basis (no need to set_default_headers, which may wrongfully impact other requests later).
- Modify the default content-type selection logic
Use a custom template, removing the "favor json" logic:
if not content_types:
return None
- for content_type in content_types:
- if re.search('json', content_type, re.IGNORECASE):
- return content_type
-
return content_types[0]
It remains to be seen what else I may be breaking here.
just ran into the same issue where the server json is just broken and I need to "enforce" xml. Poking around but I might up manipulating the swagger to to no select json at all