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?
- [ X ] Have you tested with the latest master to confirm the issue still exists?
- [ X ] Have you searched for related issues/PRs?
- [ X ] What's the actual output vs expected output?
Description
The problem. With splitOperationsByContentType=true (#23935, 7.25.0), an operation whose 200 is application/json | text/csv is generated as two methods, getReportAsJson and getReportAsCsv, each typed on its media-type. But as soon as the operation also declares a JSON error response (400: application/json), the generated getReportAsCsv sends Accept: application/json, not text/csv. A server that negotiates on Accept answers JSON (or 406) to a method that is typed to parse CSV. The option exists to make each media-type reachable, and the variant it generates cannot request its own media-type.
The same happens for java, python and spring (library: spring-cloud); details and generated code below.
Why. The split narrows only the success response of a variant; the error responses stay on every variant. produces of a CodegenOperation is the union of every response's content, so on the CSV variant it is [text/csv, application/json]. Each generator then derives the Accept header from that list with a rule that predates variants and prefers JSON:
java (okhttp-gson, resttemplate, …): selectHeaderAccept returns the first JSON entry;
python: select_header_accept does the same;
spring-cloud (Feign): produces is rendered from x-accepts, which AbstractJavaCodegen.preprocessOpenAPI computes on the original operation before the split (both variants inherit the same sorted list) and SpringMvcContract sends produces[0].
typescript-fetch is not affected: it merges the variants into overloads and reads the media-type from the x-content-type-variant-* extensions.
openapi-generator version
7.25.0 (and current master). Regression only in the sense that the feature is new: the option did not exist before 7.25.0.
OpenAPI declaration file content or url
https://gist.github.com/AntoineDuComptoirDesPharmacies/c80358f886afcae2e63613b6c09ed8af
Generation Details
java -jar openapi-generator-cli-7.25.0.jar generate -g spring --library spring-cloud \
-i report.yaml -o out/spring --global-property splitOperationsByContentType=true
java -jar openapi-generator-cli-7.25.0.jar generate -g java \
-i report.yaml -o out/java --global-property splitOperationsByContentType=true
java -jar openapi-generator-cli-7.25.0.jar generate -g python \
-i report.yaml -o out/python --global-property splitOperationsByContentType=true
Actual output
spring / spring-cloud, both variants carry the same produces, and Feign sends its first element:
@RequestMapping(method = RequestMethod.GET, value = ReportsApi.PATH_GET_REPORT_AS_CSV,
produces = { "application/json", "text/csv" })
ResponseEntity<String> getReportAsCsv(...); // sends Accept: application/json
@RequestMapping(method = RequestMethod.GET, value = ReportsApi.PATH_GET_REPORT_AS_JSON,
produces = { "application/json", "text/csv" })
ResponseEntity<Report> getReportAsJson(...);
java (okhttp-gson), the CSV variant lists JSON too, and selectHeaderAccept picks it:
// getReportAsCsvCall
final String[] localVarAccepts = { "text/csv", "application/json" }; // -> Accept: application/json
python:
# _get_report_as_csv_serialize
_header_params['Accept'] = self.api_client.select_header_accept(['text/csv', 'application/json']) # -> application/json
Expected output
Each variant produces and asks for the single media-type it was narrowed to; the error responses keep typing their body as before:
produces = { "application/json" } ResponseEntity<Report> getReportAsJson(...)
produces = { "text/csv" } ResponseEntity<String> getReportAsCsv(...)
final String[] localVarAccepts = { "text/csv" };
_header_params['Accept'] = self.api_client.select_header_accept(['text/csv'])
Operations the split leaves alone must keep the union of every response in produces, as they always had.
Related issues/PRs
Suggest a fix
A PR follows. The idea: a variant's produces is the single media-type it was narrowed to, nothing else.
- Core :
DefaultCodegen.fromOperation builds a variant's produces from its method response alone (the one the split narrowed), and getProducesInfo returns that same media-type. Every client then asks for the media-type the variant is typed on, and every server generator maps each variant on its own media-type.
- Java :
AbstractJavaCodegen stamps x-accepts / x-content-type on the variants as divideOperationsByContentType creates them. Today they are computed once in preprocessOpenAPI, before the split, so the variants inherit the values of the operation they came from.
- Unchanged : the error responses keep their own content and typing; operations the split leaves alone generate byte-identical output; the
typescript-fetch split sample regenerates without a change.
One point I would like other opinions on: for the Java Spring server, a variant then declares only its own media-type in produces (getReportAsCsv → produces = "text/csv"), while the error responses of that operation are JSON. That is what lets Spring route Accept: text/csv and Accept: application/json to the right variant without an ambiguous mapping, and the JSON error bodies still go through @ExceptionHandler / @ControllerAdvice, which Spring negotiates on the client's Accept regardless of the handler's produces. But it does mean the annotation no longer lists the error media-types. Is that acceptable, or would you rather keep them in produces some other way?
Bug Report Checklist
Description
The problem. With
splitOperationsByContentType=true(#23935, 7.25.0), an operation whose200isapplication/json | text/csvis generated as two methods,getReportAsJsonandgetReportAsCsv, each typed on its media-type. But as soon as the operation also declares a JSON error response (400: application/json), the generatedgetReportAsCsvsendsAccept: application/json, nottext/csv. A server that negotiates onAcceptanswers JSON (or 406) to a method that is typed to parse CSV. The option exists to make each media-type reachable, and the variant it generates cannot request its own media-type.The same happens for
java,pythonandspring(library: spring-cloud); details and generated code below.Why. The split narrows only the success response of a variant; the error responses stay on every variant.
producesof aCodegenOperationis the union of every response's content, so on the CSV variant it is[text/csv, application/json]. Each generator then derives theAcceptheader from that list with a rule that predates variants and prefers JSON:java(okhttp-gson, resttemplate, …):selectHeaderAcceptreturns the first JSON entry;python:select_header_acceptdoes the same;spring-cloud(Feign):producesis rendered fromx-accepts, whichAbstractJavaCodegen.preprocessOpenAPIcomputes on the original operation before the split (both variants inherit the same sorted list) andSpringMvcContractsendsproduces[0].typescript-fetchis not affected: it merges the variants into overloads and reads the media-type from thex-content-type-variant-*extensions.openapi-generator version
7.25.0 (and current master). Regression only in the sense that the feature is new: the option did not exist before 7.25.0.
OpenAPI declaration file content or url
https://gist.github.com/AntoineDuComptoirDesPharmacies/c80358f886afcae2e63613b6c09ed8af
Generation Details
Actual output
spring/spring-cloud, both variants carry the sameproduces, and Feign sends its first element:java(okhttp-gson), the CSV variant lists JSON too, andselectHeaderAcceptpicks it:python:Expected output
Each variant produces and asks for the single media-type it was narrowed to; the error responses keep typing their body as before:
Operations the split leaves alone must keep the union of every response in
produces, as they always had.Related issues/PRs
Suggest a fix
A PR follows. The idea: a variant's
producesis the single media-type it was narrowed to, nothing else.DefaultCodegen.fromOperationbuilds a variant'sproducesfrom its method response alone (the one the split narrowed), andgetProducesInforeturns that same media-type. Every client then asks for the media-type the variant is typed on, and every server generator maps each variant on its own media-type.AbstractJavaCodegenstampsx-accepts/x-content-typeon the variants asdivideOperationsByContentTypecreates them. Today they are computed once inpreprocessOpenAPI, before the split, so the variants inherit the values of the operation they came from.typescript-fetchsplit sample regenerates without a change.One point I would like other opinions on: for the Java Spring server, a variant then declares only its own media-type in
produces(getReportAsCsv→produces = "text/csv"), while the error responses of that operation are JSON. That is what lets Spring routeAccept: text/csvandAccept: application/jsonto the right variant without an ambiguous mapping, and the JSON error bodies still go through@ExceptionHandler/@ControllerAdvice, which Spring negotiates on the client'sAcceptregardless of the handler'sproduces. But it does mean the annotation no longer lists the error media-types. Is that acceptable, or would you rather keep them inproducessome other way?