[BUG][core] splitOperationsByContentType: a variant sends the Accept of another media-type when the operation has JSON error responses

Author: AntoineDuComptoirDesPharmaciesCreated Sep 15, 2026Updated Sep 15, 2026
LabelsIssue: Bug

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
bash
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:

java
@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:

java
// getReportAsCsvCall
final String[] localVarAccepts = { "text/csv", "application/json" };  // -> Accept: application/json

python:

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:

java
produces = { "application/json" }   ResponseEntity<Report> getReportAsJson(...)
produces = { "text/csv" }           ResponseEntity<String> getReportAsCsv(...)
java
final String[] localVarAccepts = { "text/csv" };
python
_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 (getReportAsCsvproduces = "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?

Source: OpenAPITools/openapi-generator