Skip to content

feat(core): add splitOperationsByContentType option to divide operati… - #23935

Merged
wing328 merged 14 commits into
OpenAPITools:masterfrom
LeComptoirDesPharmacies:issue/6708
Aug 10, 2026
Merged

feat(core): add splitOperationsByContentType option to divide operati…#23935
wing328 merged 14 commits into
OpenAPITools:masterfrom
LeComptoirDesPharmacies:issue/6708

Conversation

@AntoineDuComptoirDesPharmacies

@AntoineDuComptoirDesPharmacies AntoineDuComptoirDesPharmacies commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

This PR adds an opt-in splitOperationsByContentType option that fixes #6708: an operation may expose several request and/or response content-types with different schemas, but generators currently keep only the first one ("Multiple schemas found in the OAS 'content' section, returning only the first").
The result is a single, mistyped method, and the other content-types are unreachable.

The same underlying problem has been reported and worked around in a language-specific way in several places — e.g. #22095 (rust-axum), #8431 (Ruby), #13426 (TypeScript/Angular) and #10973 (Python request bodies). This PR addresses it generically in the core instead of per-generator.

The implementation is inspired by #19473, where operations were divided by content-type. The key difference here: the division is gated behind an opt-in option (splitOperationsByContentType, default false), so all existing generator outputs are unchanged and no current codegen is broken.

Why it is off by default

Some dynamically- or structurally-typed languages can already represent several input/return types within a single operationId, so splitting would be unnecessary (or even undesirable) for them:

Statically-typed / compiled languages (Java, C#, Go, Kotlin, Swift, ...) cannot overload a method by return type, so they benefit from one typed method per content-type — which is exactly what this option produces when enabled. Keeping it opt-in lets each consumer decide, without changing any default
output.

How it works

When the option is enabled, DefaultCodegen#preprocessOpenAPI divides an eligible operation into the cartesian product of its request-body and success-response content-types (deduplicated by schema). Each variant is a narrowed OpenAPI Operation (a single content-type on each axis) with a typed,
collision-free operationId — request appends With<Subtype> and response appends As<Subtype> (e.g. createReportWithXmlAsPdf). The variants are stored on the original operation under the x-content-type-variants extension and expanded by DefaultGenerator#processOperation, so each one re-enters
fromOperation and is typed natively by the target generator. The feature is therefore language-neutral: no per-language type re-derivation and no template changes.

Tests

Added unit tests in DefaultCodegenTest: the request×response cartesian (4 variants), the response-only case (2 variants), and a check that unambiguous operations are left untouched.

PR checklist

  • [ X ] Read the contribution guidelines.
  • [ X ] Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • [ X ] If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Technical committee mention

This feature is opt-in and primarily benefits statically-typed / compiled generators, which cannot overload a method by return type and therefore gain one typed method per content-type. cc the relevant technical committee members:


Summary by cubic

Adds an opt-in global splitOperationsByContentType to split operations by request/response content-type when schemas differ. Defaults to off; typescript-fetch merges variants into one precisely typed method, and the sample/docs were refined to show all media types and case-insensitive matching.

  • New Features

    • Global splitOperationsByContentType (default false), documented in global-properties.md; enable with --global-property splitOperationsByContentType=true.
    • Core: DefaultGenerator#processOperation delegates to the config to split; DefaultCodegen emits deduped request×response variants with collision-free ids and variant-index extensions.
    • typescript-fetch: merges variants into one method (request is a union discriminated by contentType; response typed via accept overloads). Adds ExclusiveUnion in runtime.mustache; extracts form params to apisFormParams.mustache and adds small partials to support the merge while keeping legacy output byte-identical when the option is off.
    • New sample and config: bin/configs/typescript-fetch-split-by-content-type.yaml and a typescript-fetch petstore sample with the option on; included in CI typecheck to catch regressions. Tests cover enums, forms, optional/no-body responses, and name collisions.
    • Docs/sample: merged-method docs now list the full request/response media-type union; sample switches XML to application/merge-patch+json; docs clarify this option chooses which media types get operations and does not add new encoders.
  • Bug Fixes

    • typescript-fetch: compare response Content-Type case-insensitively to pick the right deserializer.
    • Petstore resttemplate-springBoot4-jackson3 sample: set spring-web to 7.0.5 to fix build.

Written for commit eec586e. Summary will update on new commits.

Review in cubic

…ons by content-type (OpenAPITools#6708)

OpenAPI lets a single operation expose several request/response content-types with different schemas, but generators keep only the first one ("Multiple schemas found in the OAS 'content' section, returning only the first").
This yields a single, mistyped method and makes the other content-types unusable.

Add an opt-in option `splitOperationsByContentType` (default false) that divides such an operation into one operation per content-type — the cartesian product of the request-body and success-response content-types (deduplicated by schema).

The division happens at the spec level in DefaultCodegen#preprocessOpenAPI.
Each variant is a narrowed swagger Operation (single content-type on each axis) with a typed, collision-free operationId (request -> "With<Subtype>", response -> "As<Subtype>", e.g. createReportWithXmlAsPdf).
Variants are stored on the original operation under the "x-content-type-variants" extension and expanded by DefaultGenerator#processOperation, so every variant re-enters fromOperation and is typed natively by the target generator.
The feature is therefore language-neutral: no per-language type re-derivation, no template change.

Tested in DefaultCodegenTest for both the request×response cartesian (4 variants) and the response-only case (2 variants), and that unambiguous operations are left untouched.
Move the clone methods to ModelUtils to lightweight DefaultCodegen
Add new split operations option in every providers
Build project and update samples
Other solution without X variant and using divide directly while processing operations
Simplification of code, removing useless "findMultiSchemaSuccessResponseCode"

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 138 files

Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.

Re-trigger cubic

Remove default implemenation in Interface. No sense because the default implementation is in CodegenDefault
@AntoineDuComptoirDesPharmacies

Copy link
Copy Markdown
Contributor Author

Hello @wing328,

I know you express your good feeling about dividing operations to manage this use case. (See : #19473)

Do not hesisaste to give me your point of view about this big change (opt-in) that would solve the problem for multiple langages.

Yours faithfully,
LCDP

@wing328

wing328 commented Aug 4, 2026

Copy link
Copy Markdown
Member

please PM me via Slack to further discuss this to move this PR forward when you've time.

https://join.slack.com/t/openapi-generator/shared_invite/zt-36ucx4ybl-jYrN6euoYn6zxXNZdldoZA

…ty, merged back per generator

Following the review on OpenAPITools#23935: the option was a CLI option repeated in every
generator that wanted it. It is now a global property, read once from
GlobalSettings in DefaultCodegen.processOpts, like skipFormModel already is.

DefaultGenerator.processOperation asks the config to divide an operation before
processing it, and DefaultCodegen implements the division once, language
neutrally: an operation whose request body and/or method response expose several
content-types with different schemas becomes one operation per (request,
response) content-type pair, each narrowed to a single media-type and given a
typed, collision-free operationId. Every variant re-enters fromOperation, so its
body and return types are resolved natively by the target generator - the shared
code re-derives no types of its own.

Each variant also carries x-content-type-variant-* extensions recording where it
sits in the matrix, so a generator able to express the whole matrix in a single
construct can merge the variants back instead of emitting one method per
combination. typescript-fetch does: the variants collapse into one method whose
request type is a union discriminated by `contentType`, and whose return type is
picked by overloads on `accept`, each branch keeping the types the split
resolved for it. A generator that does not merge simply gets the separate
methods, which is what a statically-typed language needs anyway.

With the option off, divideOperationsByContentType returns the operation as a
singleton and the merge returns immediately: regenerating the 19 typescript-fetch
sample configs produces a byte-identical tree.
No textual conflict. Verified against the merged tree: the full openapi-generator
test suite passes (4335 tests), regenerating the 19 typescript-fetch sample
configs leaves them byte-identical to master, and the issue6708-* specs generated
with splitOperationsByContentType=true still compile under tsc --strict.

Also folded in here: TypeScriptFetchClientCodegen's LOGGER was declared static,
which ArchUnitRulesTest forbids for codegen classes. It is now a plain instance
field, like the other TypeScript generators declare theirs.
Three conflicts, all on files this branch owns:

- the two static imports simply coexist;
- addMultipartFileArrayApiExampleValues, new upstream, runs before
  mergeContentTypeVariants. The merge has to stay last: it drops the non-default
  variants from the operation list while the merged operation keeps referencing
  their parameters and return types, so every other pass must have seen them
  first;
- apis.mustache's form-params block now lives in apisFormParams.mustache, shared
  by the legacy path and the content-type switch. Upstream's change to that block
  (isFreeFormObject -> runtime.anyToJSON) was carried into the partial, which
  stays a verbatim copy of upstream's text at the method's indentation - that is
  what keeps the legacy path byte-identical.

Verified on the merged tree: the full openapi-generator test suite passes (4676
tests), regenerating the 19 typescript-fetch sample configs leaves them
byte-identical to upstream/master, and the issue6708-* specs generated with
splitOperationsByContentType=true compile under tsc --strict.
…option on

Nothing committed showed what splitOperationsByContentType emits, and nothing in
CI compiled it: the option was exercised only by unit tests asserting on strings
in a temp directory. Reviewers had to build the branch to see the feature, and a
regression that produced uncompilable TypeScript would have gone unnoticed - the
review of this branch found four of those.

The sample's spec gathers the shapes the option has to handle: a response-only
split, a split on both axes, a multipart body whose operation is split on the
response axis only, a request split mixing JSON and multipart, and an enum
parameter carried by a split operation.

bin/ts-typecheck-all.sh discovers samples on its own - any generated directory
holding both a tsconfig.json and a package.json is typechecked - so setting
npmName is all it takes for CI to compile this one. No workflow change needed.

Writing the sample immediately paid for itself: the form body assembled inside
the content-type switch was under-indented by eight columns. IndentedLambda
leaves the first line alone, and moving the partial to column zero in the
previous commit removed the literal spaces that used to indent it.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 30 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

…mple

Three of the four points raised were about the sample, which is what the sample
is for - the output is readable now.

A media type is case-insensitive (RFC 9110), so a server answering
`Application/PDF` fell through the dispatch chain and had its body decoded as
the default content-type. Both sides of the comparison are lower-cased now.

The merged operation only advertised one media type per axis: the split narrows
each variant to a single one and the merge never put them back, so the generated
documentation hid that createReport also accepts a patch body and can answer
with a PDF. The union is restored on the merged operation. apis.mustache reads
consumes only where the request axis was not split - a case where the union is
the single value anyway - and never reads produces, so nothing but the
documentation changes.

The sample demonstrated the request axis with an `application/xml` body backed
by an object schema. typescript-fetch has no XML serialiser and JSON-encodes
that body under an XML Content-Type - with or without this option, as generating
the same spec with the option off shows. The behaviour is not this option's
doing, but advertising it in the sample promised something the generator does
not deliver, so the sample now splits on `application/merge-patch+json`, which
it does. The limitation is stated in docs/global-properties.md instead: the
option decides which content-types get an operation, not how a body is encoded.

The remaining point, body serialisation reading the pre-override header map in
runtime.ts, is upstream code this branch does not touch; it applies to every
typescript-fetch client and belongs in its own change.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 20 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java:1093">
P3: Generated API documentation can list response media types in a different order from the OpenAPI response declaration when another response status has content: each narrowed variant carries all response-status media types, so `mediaTypesOf` interleaves them as it walks variants. Preserving the pre-split response media-type order would avoid this documentation regression.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

// request axis was not split - a case where this union is the single value anyway - and never
// reads produces, so this is documentation only.
base.consumes = mediaTypesOf(requestVariants, v -> v.consumes);
base.produces = mediaTypesOf(responseVariants, v -> v.produces);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Generated API documentation can list response media types in a different order from the OpenAPI response declaration when another response status has content: each narrowed variant carries all response-status media types, so mediaTypesOf interleaves them as it walks variants. Preserving the pre-split response media-type order would avoid this documentation regression.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java, line 1093:

<comment>Generated API documentation can list response media types in a different order from the OpenAPI response declaration when another response status has content: each narrowed variant carries all response-status media types, so `mediaTypesOf` interleaves them as it walks variants. Preserving the pre-split response media-type order would avoid this documentation regression.</comment>

<file context>
@@ -1084,13 +1085,39 @@ private void mergeContentTypeVariants(OperationsMap operations) {
+            // request axis was not split - a case where this union is the single value anyway - and never
+            // reads produces, so this is documentation only.
+            base.consumes = mediaTypesOf(requestVariants, v -> v.consumes);
+            base.produces = mediaTypesOf(responseVariants, v -> v.produces);
+
             variants.stream().filter(op -> op != base).forEach(superseded::add);
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only mixing the order of generation, only esthetic, i propose not to fix this.

@wing328

wing328 commented Aug 10, 2026

Copy link
Copy Markdown
Member

ran some tests locally and the results are good

let's give it a try

thanks for your contribution.

@wing328
wing328 merged commit 00e668a into OpenAPITools:master Aug 10, 2026
168 of 169 checks passed
@wing328 wing328 added this to the 7.25.0 milestone Aug 10, 2026
b2l added a commit to LeComptoirDesPharmacies/openapi-generator that referenced this pull request Aug 12, 2026
… guards into the date helpers

Upstream OpenAPITools#24509 null-guarded required date properties; keep that guard
but route the conversion through parseDate/serializeDate. Port the form
param date handling into the apisFormParams partial extracted by OpenAPITools#23935,
and regenerate the affected samples.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG][JAVA][SPRING] Endpoints don't support different schema per content-type

2 participants