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

@AntoineDuComptoirDesPharmaciesAntoineDuComptoirDesPharmacies 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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-aicubic-dev-aiBot 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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:masterAug 10, 2026
168 of 169 checks passed
@wing328wing328 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>
wing328 added a commit that referenced this pull request Aug 17, 2026
* [typescript-fetch] centralise date handling, add dateLibrary, fix format: date
Date handling was spread across four templates, each inlining its own
expression. That had three consequences:
1. `format: date` was not handled at all for form parameters, so a Date
was appended raw and stringified by the browser
("Wed Aug 05 2026 00:00:00 GMT+0200 (…)") instead of "2026-08-05".
2. `format: date` shifted by a day everywhere except UTC, in one direction
or the other. Parsing (`new Date('2026-08-05')`) and serialising
(`.toISOString().substring(0, 10)`) both work in UTC, but consumers do
not: a date picker builds local midnight and display reads local
getters. West of UTC a date from the API displays as the previous day;
east of UTC a locally built date is sent as the previous day. An RFC
3339 full-date has no offset, so both ends have to use the same wall
clock — they now both use the local calendar. `format: date-time` is a
genuine instant and stays UTC.
3. Whether dates were represented as Date or string was decided by
`withoutRuntimeChecks`, an unrelated flag about payload validation.
All call sites (models, oneOf models, path/query/form parameters and the
querystring helper) now route through serializeDate/serializeDateTime and
parseDate/parseDateTime in runtime.ts, so the representation is defined in
one place. The new `dateLibrary` option (`date`, the default and previous
behaviour, or `string`) makes the choice explicit; `withoutRuntimeChecks`
implies `string`, as before, since there is no model code left to convert
with.
Adds a spec fixture covering every location a date can appear in, two
sample builds (one per option value), and tests for the option, the
fallback and the serialisation semantics.
* [typescript-fetch] address review: ES6 target, year 0-99, invalid dates, unused imports
- serializeDate no longer uses padStart, which is ES2017: the es6-target
sample did not compile against its own tsconfig.
- parseDate builds the local date with setFullYear, so years 0000-0099 keep
their century instead of picking up the multi-argument Date constructor's
1900 offset ("0045-08-05" was parsed as 1945).
- parseDate rejects components that roll over, so an out-of-range date or a
day the local zone skipped returns Invalid Date rather than a plausible
wrong one. Previously "2026-13-45" became 2027-02-14.
- serializeDate throws RangeError on an invalid Date instead of emitting
"0NaN-NaN-NaN", matching serializeDateTime.
- Models without a date property no longer import the date helpers, via a
new x-hasDateVars extension mirroring the template's own branches. This
reverts most of the sample churn from the previous commit.
Drops testDateFormatUsesTheLocalCalendar: runtime.ts is identical for every
spec, so asserting its body only restated the template. The per-build
tsconfig typecheck covers the ES6 regression properly.
* [typescript-fetch] align the oneOf date guards with the date helpers
The oneOf branches tested a value with `new Date(json)` but converted it with
parseDate, so the two could disagree: "2026-02-30" passes the lenient test (V8
rolls it to March 2) and then parseDate rejects it, leaving the branch selected
and returning Invalid Date. Testing with the same helper that does the
conversion lets the oneOf fall through to another branch instead.
Adds a oneOf date member to the date-handling fixture. No sample in the repo
exercised these branches, so the generated form of the guard was invisible in
the samples; only the scalar variant is left out, because a scalar oneOf
primitive already fails `tsc --strict` on master (it can return undefined,
which is not in the union).
Drops the comment justifying the ES6-safe padding: the per-build tsconfig
typecheck already fails if someone reaches for padStart again.
* [typescript-fetch] rebase on master: fold the #24509 null guards into the date helpers
Upstream #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 #23935,
and regenerate the affected samples.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
* update TS samples
---------
Co-authored-by: Nicolas Medda <nicolas@lecomptoirdespharmacies.fr>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nicolas Medda <b2l.powa@gmail.com>
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
@jschonenberg

Copy link
Copy Markdown

@AntoineDuComptoirDesPharmacies thank you for your work!

Since splitOperationsByContentType was added as a global property, I would assume that it would automatically be available for all generators.
However, we are using the java generator and the property does not seem to be effective.

Is this as expected? Is it due to how the java generator is implemented?

Sign up for freeto 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

3 participants

@AntoineDuComptoirDesPharmacies@wing328@jschonenberg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@AntoineDuComptoirDesPharmaciesAntoineDuComptoirDesPharmacies 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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-aicubic-dev-aiBot 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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:masterAug 10, 2026
168 of 169 checks passed
@wing328wing328 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>
wing328 added a commit that referenced this pull request Aug 17, 2026
* [typescript-fetch] centralise date handling, add dateLibrary, fix format: date
Date handling was spread across four templates, each inlining its own
expression. That had three consequences:
1. `format: date` was not handled at all for form parameters, so a Date
was appended raw and stringified by the browser
("Wed Aug 05 2026 00:00:00 GMT+0200 (…)") instead of "2026-08-05".
2. `format: date` shifted by a day everywhere except UTC, in one direction
or the other. Parsing (`new Date('2026-08-05')`) and serialising
(`.toISOString().substring(0, 10)`) both work in UTC, but consumers do
not: a date picker builds local midnight and display reads local
getters. West of UTC a date from the API displays as the previous day;
east of UTC a locally built date is sent as the previous day. An RFC
3339 full-date has no offset, so both ends have to use the same wall
clock — they now both use the local calendar. `format: date-time` is a
genuine instant and stays UTC.
3. Whether dates were represented as Date or string was decided by
`withoutRuntimeChecks`, an unrelated flag about payload validation.
All call sites (models, oneOf models, path/query/form parameters and the
querystring helper) now route through serializeDate/serializeDateTime and
parseDate/parseDateTime in runtime.ts, so the representation is defined in
one place. The new `dateLibrary` option (`date`, the default and previous
behaviour, or `string`) makes the choice explicit; `withoutRuntimeChecks`
implies `string`, as before, since there is no model code left to convert
with.
Adds a spec fixture covering every location a date can appear in, two
sample builds (one per option value), and tests for the option, the
fallback and the serialisation semantics.
* [typescript-fetch] address review: ES6 target, year 0-99, invalid dates, unused imports
- serializeDate no longer uses padStart, which is ES2017: the es6-target
sample did not compile against its own tsconfig.
- parseDate builds the local date with setFullYear, so years 0000-0099 keep
their century instead of picking up the multi-argument Date constructor's
1900 offset ("0045-08-05" was parsed as 1945).
- parseDate rejects components that roll over, so an out-of-range date or a
day the local zone skipped returns Invalid Date rather than a plausible
wrong one. Previously "2026-13-45" became 2027-02-14.
- serializeDate throws RangeError on an invalid Date instead of emitting
"0NaN-NaN-NaN", matching serializeDateTime.
- Models without a date property no longer import the date helpers, via a
new x-hasDateVars extension mirroring the template's own branches. This
reverts most of the sample churn from the previous commit.
Drops testDateFormatUsesTheLocalCalendar: runtime.ts is identical for every
spec, so asserting its body only restated the template. The per-build
tsconfig typecheck covers the ES6 regression properly.
* [typescript-fetch] align the oneOf date guards with the date helpers
The oneOf branches tested a value with `new Date(json)` but converted it with
parseDate, so the two could disagree: "2026-02-30" passes the lenient test (V8
rolls it to March 2) and then parseDate rejects it, leaving the branch selected
and returning Invalid Date. Testing with the same helper that does the
conversion lets the oneOf fall through to another branch instead.
Adds a oneOf date member to the date-handling fixture. No sample in the repo
exercised these branches, so the generated form of the guard was invisible in
the samples; only the scalar variant is left out, because a scalar oneOf
primitive already fails `tsc --strict` on master (it can return undefined,
which is not in the union).
Drops the comment justifying the ES6-safe padding: the per-build tsconfig
typecheck already fails if someone reaches for padStart again.
* [typescript-fetch] rebase on master: fold the #24509 null guards into the date helpers
Upstream #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 #23935,
and regenerate the affected samples.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
* update TS samples
---------
Co-authored-by: Nicolas Medda <nicolas@lecomptoirdespharmacies.fr>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nicolas Medda <b2l.powa@gmail.com>
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
@jschonenberg

Copy link
Copy Markdown

@AntoineDuComptoirDesPharmacies thank you for your work!

Since splitOperationsByContentType was added as a global property, I would assume that it would automatically be available for all generators.
However, we are using the java generator and the property does not seem to be effective.

Is this as expected? Is it due to how the java generator is implemented?

Sign up for freeto 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

3 participants

@AntoineDuComptoirDesPharmacies@wing328@jschonenberg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@AntoineDuComptoirDesPharmaciesAntoineDuComptoirDesPharmacies 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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-aicubic-dev-aiBot 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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:masterAug 10, 2026
168 of 169 checks passed
@wing328wing328 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>
wing328 added a commit that referenced this pull request Aug 17, 2026
* [typescript-fetch] centralise date handling, add dateLibrary, fix format: date
Date handling was spread across four templates, each inlining its own
expression. That had three consequences:
1. `format: date` was not handled at all for form parameters, so a Date
was appended raw and stringified by the browser
("Wed Aug 05 2026 00:00:00 GMT+0200 (…)") instead of "2026-08-05".
2. `format: date` shifted by a day everywhere except UTC, in one direction
or the other. Parsing (`new Date('2026-08-05')`) and serialising
(`.toISOString().substring(0, 10)`) both work in UTC, but consumers do
not: a date picker builds local midnight and display reads local
getters. West of UTC a date from the API displays as the previous day;
east of UTC a locally built date is sent as the previous day. An RFC
3339 full-date has no offset, so both ends have to use the same wall
clock — they now both use the local calendar. `format: date-time` is a
genuine instant and stays UTC.
3. Whether dates were represented as Date or string was decided by
`withoutRuntimeChecks`, an unrelated flag about payload validation.
All call sites (models, oneOf models, path/query/form parameters and the
querystring helper) now route through serializeDate/serializeDateTime and
parseDate/parseDateTime in runtime.ts, so the representation is defined in
one place. The new `dateLibrary` option (`date`, the default and previous
behaviour, or `string`) makes the choice explicit; `withoutRuntimeChecks`
implies `string`, as before, since there is no model code left to convert
with.
Adds a spec fixture covering every location a date can appear in, two
sample builds (one per option value), and tests for the option, the
fallback and the serialisation semantics.
* [typescript-fetch] address review: ES6 target, year 0-99, invalid dates, unused imports
- serializeDate no longer uses padStart, which is ES2017: the es6-target
sample did not compile against its own tsconfig.
- parseDate builds the local date with setFullYear, so years 0000-0099 keep
their century instead of picking up the multi-argument Date constructor's
1900 offset ("0045-08-05" was parsed as 1945).
- parseDate rejects components that roll over, so an out-of-range date or a
day the local zone skipped returns Invalid Date rather than a plausible
wrong one. Previously "2026-13-45" became 2027-02-14.
- serializeDate throws RangeError on an invalid Date instead of emitting
"0NaN-NaN-NaN", matching serializeDateTime.
- Models without a date property no longer import the date helpers, via a
new x-hasDateVars extension mirroring the template's own branches. This
reverts most of the sample churn from the previous commit.
Drops testDateFormatUsesTheLocalCalendar: runtime.ts is identical for every
spec, so asserting its body only restated the template. The per-build
tsconfig typecheck covers the ES6 regression properly.
* [typescript-fetch] align the oneOf date guards with the date helpers
The oneOf branches tested a value with `new Date(json)` but converted it with
parseDate, so the two could disagree: "2026-02-30" passes the lenient test (V8
rolls it to March 2) and then parseDate rejects it, leaving the branch selected
and returning Invalid Date. Testing with the same helper that does the
conversion lets the oneOf fall through to another branch instead.
Adds a oneOf date member to the date-handling fixture. No sample in the repo
exercised these branches, so the generated form of the guard was invisible in
the samples; only the scalar variant is left out, because a scalar oneOf
primitive already fails `tsc --strict` on master (it can return undefined,
which is not in the union).
Drops the comment justifying the ES6-safe padding: the per-build tsconfig
typecheck already fails if someone reaches for padStart again.
* [typescript-fetch] rebase on master: fold the #24509 null guards into the date helpers
Upstream #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 #23935,
and regenerate the affected samples.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
* update TS samples
---------
Co-authored-by: Nicolas Medda <nicolas@lecomptoirdespharmacies.fr>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nicolas Medda <b2l.powa@gmail.com>
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
@jschonenberg

Copy link
Copy Markdown

@AntoineDuComptoirDesPharmacies thank you for your work!

Since splitOperationsByContentType was added as a global property, I would assume that it would automatically be available for all generators.
However, we are using the java generator and the property does not seem to be effective.

Is this as expected? Is it due to how the java generator is implemented?

Sign up for freeto 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

3 participants

@AntoineDuComptoirDesPharmacies@wing328@jschonenberg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@AntoineDuComptoirDesPharmaciesAntoineDuComptoirDesPharmacies 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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-aicubic-dev-aiBot 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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:masterAug 10, 2026
168 of 169 checks passed
@wing328wing328 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>
wing328 added a commit that referenced this pull request Aug 17, 2026
* [typescript-fetch] centralise date handling, add dateLibrary, fix format: date
Date handling was spread across four templates, each inlining its own
expression. That had three consequences:
1. `format: date` was not handled at all for form parameters, so a Date
was appended raw and stringified by the browser
("Wed Aug 05 2026 00:00:00 GMT+0200 (…)") instead of "2026-08-05".
2. `format: date` shifted by a day everywhere except UTC, in one direction
or the other. Parsing (`new Date('2026-08-05')`) and serialising
(`.toISOString().substring(0, 10)`) both work in UTC, but consumers do
not: a date picker builds local midnight and display reads local
getters. West of UTC a date from the API displays as the previous day;
east of UTC a locally built date is sent as the previous day. An RFC
3339 full-date has no offset, so both ends have to use the same wall
clock — they now both use the local calendar. `format: date-time` is a
genuine instant and stays UTC.
3. Whether dates were represented as Date or string was decided by
`withoutRuntimeChecks`, an unrelated flag about payload validation.
All call sites (models, oneOf models, path/query/form parameters and the
querystring helper) now route through serializeDate/serializeDateTime and
parseDate/parseDateTime in runtime.ts, so the representation is defined in
one place. The new `dateLibrary` option (`date`, the default and previous
behaviour, or `string`) makes the choice explicit; `withoutRuntimeChecks`
implies `string`, as before, since there is no model code left to convert
with.
Adds a spec fixture covering every location a date can appear in, two
sample builds (one per option value), and tests for the option, the
fallback and the serialisation semantics.
* [typescript-fetch] address review: ES6 target, year 0-99, invalid dates, unused imports
- serializeDate no longer uses padStart, which is ES2017: the es6-target
sample did not compile against its own tsconfig.
- parseDate builds the local date with setFullYear, so years 0000-0099 keep
their century instead of picking up the multi-argument Date constructor's
1900 offset ("0045-08-05" was parsed as 1945).
- parseDate rejects components that roll over, so an out-of-range date or a
day the local zone skipped returns Invalid Date rather than a plausible
wrong one. Previously "2026-13-45" became 2027-02-14.
- serializeDate throws RangeError on an invalid Date instead of emitting
"0NaN-NaN-NaN", matching serializeDateTime.
- Models without a date property no longer import the date helpers, via a
new x-hasDateVars extension mirroring the template's own branches. This
reverts most of the sample churn from the previous commit.
Drops testDateFormatUsesTheLocalCalendar: runtime.ts is identical for every
spec, so asserting its body only restated the template. The per-build
tsconfig typecheck covers the ES6 regression properly.
* [typescript-fetch] align the oneOf date guards with the date helpers
The oneOf branches tested a value with `new Date(json)` but converted it with
parseDate, so the two could disagree: "2026-02-30" passes the lenient test (V8
rolls it to March 2) and then parseDate rejects it, leaving the branch selected
and returning Invalid Date. Testing with the same helper that does the
conversion lets the oneOf fall through to another branch instead.
Adds a oneOf date member to the date-handling fixture. No sample in the repo
exercised these branches, so the generated form of the guard was invisible in
the samples; only the scalar variant is left out, because a scalar oneOf
primitive already fails `tsc --strict` on master (it can return undefined,
which is not in the union).
Drops the comment justifying the ES6-safe padding: the per-build tsconfig
typecheck already fails if someone reaches for padStart again.
* [typescript-fetch] rebase on master: fold the #24509 null guards into the date helpers
Upstream #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 #23935,
and regenerate the affected samples.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
* update TS samples
---------
Co-authored-by: Nicolas Medda <nicolas@lecomptoirdespharmacies.fr>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nicolas Medda <b2l.powa@gmail.com>
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
@jschonenberg

Copy link
Copy Markdown

@AntoineDuComptoirDesPharmacies thank you for your work!

Since splitOperationsByContentType was added as a global property, I would assume that it would automatically be available for all generators.
However, we are using the java generator and the property does not seem to be effective.

Is this as expected? Is it due to how the java generator is implemented?

Sign up for freeto 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

3 participants

@AntoineDuComptoirDesPharmacies@wing328@jschonenberg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@AntoineDuComptoirDesPharmaciesAntoineDuComptoirDesPharmacies 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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-aicubic-dev-aiBot 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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:masterAug 10, 2026
168 of 169 checks passed
@wing328wing328 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>
wing328 added a commit that referenced this pull request Aug 17, 2026
* [typescript-fetch] centralise date handling, add dateLibrary, fix format: date
Date handling was spread across four templates, each inlining its own
expression. That had three consequences:
1. `format: date` was not handled at all for form parameters, so a Date
was appended raw and stringified by the browser
("Wed Aug 05 2026 00:00:00 GMT+0200 (…)") instead of "2026-08-05".
2. `format: date` shifted by a day everywhere except UTC, in one direction
or the other. Parsing (`new Date('2026-08-05')`) and serialising
(`.toISOString().substring(0, 10)`) both work in UTC, but consumers do
not: a date picker builds local midnight and display reads local
getters. West of UTC a date from the API displays as the previous day;
east of UTC a locally built date is sent as the previous day. An RFC
3339 full-date has no offset, so both ends have to use the same wall
clock — they now both use the local calendar. `format: date-time` is a
genuine instant and stays UTC.
3. Whether dates were represented as Date or string was decided by
`withoutRuntimeChecks`, an unrelated flag about payload validation.
All call sites (models, oneOf models, path/query/form parameters and the
querystring helper) now route through serializeDate/serializeDateTime and
parseDate/parseDateTime in runtime.ts, so the representation is defined in
one place. The new `dateLibrary` option (`date`, the default and previous
behaviour, or `string`) makes the choice explicit; `withoutRuntimeChecks`
implies `string`, as before, since there is no model code left to convert
with.
Adds a spec fixture covering every location a date can appear in, two
sample builds (one per option value), and tests for the option, the
fallback and the serialisation semantics.
* [typescript-fetch] address review: ES6 target, year 0-99, invalid dates, unused imports
- serializeDate no longer uses padStart, which is ES2017: the es6-target
sample did not compile against its own tsconfig.
- parseDate builds the local date with setFullYear, so years 0000-0099 keep
their century instead of picking up the multi-argument Date constructor's
1900 offset ("0045-08-05" was parsed as 1945).
- parseDate rejects components that roll over, so an out-of-range date or a
day the local zone skipped returns Invalid Date rather than a plausible
wrong one. Previously "2026-13-45" became 2027-02-14.
- serializeDate throws RangeError on an invalid Date instead of emitting
"0NaN-NaN-NaN", matching serializeDateTime.
- Models without a date property no longer import the date helpers, via a
new x-hasDateVars extension mirroring the template's own branches. This
reverts most of the sample churn from the previous commit.
Drops testDateFormatUsesTheLocalCalendar: runtime.ts is identical for every
spec, so asserting its body only restated the template. The per-build
tsconfig typecheck covers the ES6 regression properly.
* [typescript-fetch] align the oneOf date guards with the date helpers
The oneOf branches tested a value with `new Date(json)` but converted it with
parseDate, so the two could disagree: "2026-02-30" passes the lenient test (V8
rolls it to March 2) and then parseDate rejects it, leaving the branch selected
and returning Invalid Date. Testing with the same helper that does the
conversion lets the oneOf fall through to another branch instead.
Adds a oneOf date member to the date-handling fixture. No sample in the repo
exercised these branches, so the generated form of the guard was invisible in
the samples; only the scalar variant is left out, because a scalar oneOf
primitive already fails `tsc --strict` on master (it can return undefined,
which is not in the union).
Drops the comment justifying the ES6-safe padding: the per-build tsconfig
typecheck already fails if someone reaches for padStart again.
* [typescript-fetch] rebase on master: fold the #24509 null guards into the date helpers
Upstream #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 #23935,
and regenerate the affected samples.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
* update TS samples
---------
Co-authored-by: Nicolas Medda <nicolas@lecomptoirdespharmacies.fr>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nicolas Medda <b2l.powa@gmail.com>
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
@jschonenberg

Copy link
Copy Markdown

@AntoineDuComptoirDesPharmacies thank you for your work!

Since splitOperationsByContentType was added as a global property, I would assume that it would automatically be available for all generators.
However, we are using the java generator and the property does not seem to be effective.

Is this as expected? Is it due to how the java generator is implemented?

Sign up for freeto 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

3 participants

@AntoineDuComptoirDesPharmacies@wing328@jschonenberg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@AntoineDuComptoirDesPharmaciesAntoineDuComptoirDesPharmacies 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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-aicubic-dev-aiBot 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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:masterAug 10, 2026
168 of 169 checks passed
@wing328wing328 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>
wing328 added a commit that referenced this pull request Aug 17, 2026
* [typescript-fetch] centralise date handling, add dateLibrary, fix format: date
Date handling was spread across four templates, each inlining its own
expression. That had three consequences:
1. `format: date` was not handled at all for form parameters, so a Date
was appended raw and stringified by the browser
("Wed Aug 05 2026 00:00:00 GMT+0200 (…)") instead of "2026-08-05".
2. `format: date` shifted by a day everywhere except UTC, in one direction
or the other. Parsing (`new Date('2026-08-05')`) and serialising
(`.toISOString().substring(0, 10)`) both work in UTC, but consumers do
not: a date picker builds local midnight and display reads local
getters. West of UTC a date from the API displays as the previous day;
east of UTC a locally built date is sent as the previous day. An RFC
3339 full-date has no offset, so both ends have to use the same wall
clock — they now both use the local calendar. `format: date-time` is a
genuine instant and stays UTC.
3. Whether dates were represented as Date or string was decided by
`withoutRuntimeChecks`, an unrelated flag about payload validation.
All call sites (models, oneOf models, path/query/form parameters and the
querystring helper) now route through serializeDate/serializeDateTime and
parseDate/parseDateTime in runtime.ts, so the representation is defined in
one place. The new `dateLibrary` option (`date`, the default and previous
behaviour, or `string`) makes the choice explicit; `withoutRuntimeChecks`
implies `string`, as before, since there is no model code left to convert
with.
Adds a spec fixture covering every location a date can appear in, two
sample builds (one per option value), and tests for the option, the
fallback and the serialisation semantics.
* [typescript-fetch] address review: ES6 target, year 0-99, invalid dates, unused imports
- serializeDate no longer uses padStart, which is ES2017: the es6-target
sample did not compile against its own tsconfig.
- parseDate builds the local date with setFullYear, so years 0000-0099 keep
their century instead of picking up the multi-argument Date constructor's
1900 offset ("0045-08-05" was parsed as 1945).
- parseDate rejects components that roll over, so an out-of-range date or a
day the local zone skipped returns Invalid Date rather than a plausible
wrong one. Previously "2026-13-45" became 2027-02-14.
- serializeDate throws RangeError on an invalid Date instead of emitting
"0NaN-NaN-NaN", matching serializeDateTime.
- Models without a date property no longer import the date helpers, via a
new x-hasDateVars extension mirroring the template's own branches. This
reverts most of the sample churn from the previous commit.
Drops testDateFormatUsesTheLocalCalendar: runtime.ts is identical for every
spec, so asserting its body only restated the template. The per-build
tsconfig typecheck covers the ES6 regression properly.
* [typescript-fetch] align the oneOf date guards with the date helpers
The oneOf branches tested a value with `new Date(json)` but converted it with
parseDate, so the two could disagree: "2026-02-30" passes the lenient test (V8
rolls it to March 2) and then parseDate rejects it, leaving the branch selected
and returning Invalid Date. Testing with the same helper that does the
conversion lets the oneOf fall through to another branch instead.
Adds a oneOf date member to the date-handling fixture. No sample in the repo
exercised these branches, so the generated form of the guard was invisible in
the samples; only the scalar variant is left out, because a scalar oneOf
primitive already fails `tsc --strict` on master (it can return undefined,
which is not in the union).
Drops the comment justifying the ES6-safe padding: the per-build tsconfig
typecheck already fails if someone reaches for padStart again.
* [typescript-fetch] rebase on master: fold the #24509 null guards into the date helpers
Upstream #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 #23935,
and regenerate the affected samples.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
* update TS samples
---------
Co-authored-by: Nicolas Medda <nicolas@lecomptoirdespharmacies.fr>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nicolas Medda <b2l.powa@gmail.com>
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
@jschonenberg

Copy link
Copy Markdown

@AntoineDuComptoirDesPharmacies thank you for your work!

Since splitOperationsByContentType was added as a global property, I would assume that it would automatically be available for all generators.
However, we are using the java generator and the property does not seem to be effective.

Is this as expected? Is it due to how the java generator is implemented?

Sign up for freeto 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

3 participants

@AntoineDuComptoirDesPharmacies@wing328@jschonenberg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@AntoineDuComptoirDesPharmaciesAntoineDuComptoirDesPharmacies 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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-aicubic-dev-aiBot 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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:masterAug 10, 2026
168 of 169 checks passed
@wing328wing328 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>
wing328 added a commit that referenced this pull request Aug 17, 2026
* [typescript-fetch] centralise date handling, add dateLibrary, fix format: date
Date handling was spread across four templates, each inlining its own
expression. That had three consequences:
1. `format: date` was not handled at all for form parameters, so a Date
was appended raw and stringified by the browser
("Wed Aug 05 2026 00:00:00 GMT+0200 (…)") instead of "2026-08-05".
2. `format: date` shifted by a day everywhere except UTC, in one direction
or the other. Parsing (`new Date('2026-08-05')`) and serialising
(`.toISOString().substring(0, 10)`) both work in UTC, but consumers do
not: a date picker builds local midnight and display reads local
getters. West of UTC a date from the API displays as the previous day;
east of UTC a locally built date is sent as the previous day. An RFC
3339 full-date has no offset, so both ends have to use the same wall
clock — they now both use the local calendar. `format: date-time` is a
genuine instant and stays UTC.
3. Whether dates were represented as Date or string was decided by
`withoutRuntimeChecks`, an unrelated flag about payload validation.
All call sites (models, oneOf models, path/query/form parameters and the
querystring helper) now route through serializeDate/serializeDateTime and
parseDate/parseDateTime in runtime.ts, so the representation is defined in
one place. The new `dateLibrary` option (`date`, the default and previous
behaviour, or `string`) makes the choice explicit; `withoutRuntimeChecks`
implies `string`, as before, since there is no model code left to convert
with.
Adds a spec fixture covering every location a date can appear in, two
sample builds (one per option value), and tests for the option, the
fallback and the serialisation semantics.
* [typescript-fetch] address review: ES6 target, year 0-99, invalid dates, unused imports
- serializeDate no longer uses padStart, which is ES2017: the es6-target
sample did not compile against its own tsconfig.
- parseDate builds the local date with setFullYear, so years 0000-0099 keep
their century instead of picking up the multi-argument Date constructor's
1900 offset ("0045-08-05" was parsed as 1945).
- parseDate rejects components that roll over, so an out-of-range date or a
day the local zone skipped returns Invalid Date rather than a plausible
wrong one. Previously "2026-13-45" became 2027-02-14.
- serializeDate throws RangeError on an invalid Date instead of emitting
"0NaN-NaN-NaN", matching serializeDateTime.
- Models without a date property no longer import the date helpers, via a
new x-hasDateVars extension mirroring the template's own branches. This
reverts most of the sample churn from the previous commit.
Drops testDateFormatUsesTheLocalCalendar: runtime.ts is identical for every
spec, so asserting its body only restated the template. The per-build
tsconfig typecheck covers the ES6 regression properly.
* [typescript-fetch] align the oneOf date guards with the date helpers
The oneOf branches tested a value with `new Date(json)` but converted it with
parseDate, so the two could disagree: "2026-02-30" passes the lenient test (V8
rolls it to March 2) and then parseDate rejects it, leaving the branch selected
and returning Invalid Date. Testing with the same helper that does the
conversion lets the oneOf fall through to another branch instead.
Adds a oneOf date member to the date-handling fixture. No sample in the repo
exercised these branches, so the generated form of the guard was invisible in
the samples; only the scalar variant is left out, because a scalar oneOf
primitive already fails `tsc --strict` on master (it can return undefined,
which is not in the union).
Drops the comment justifying the ES6-safe padding: the per-build tsconfig
typecheck already fails if someone reaches for padStart again.
* [typescript-fetch] rebase on master: fold the #24509 null guards into the date helpers
Upstream #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 #23935,
and regenerate the affected samples.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
* update TS samples
---------
Co-authored-by: Nicolas Medda <nicolas@lecomptoirdespharmacies.fr>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nicolas Medda <b2l.powa@gmail.com>
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
@jschonenberg

Copy link
Copy Markdown

@AntoineDuComptoirDesPharmacies thank you for your work!

Since splitOperationsByContentType was added as a global property, I would assume that it would automatically be available for all generators.
However, we are using the java generator and the property does not seem to be effective.

Is this as expected? Is it due to how the java generator is implemented?

Sign up for freeto 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

3 participants

@AntoineDuComptoirDesPharmacies@wing328@jschonenberg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@AntoineDuComptoirDesPharmaciesAntoineDuComptoirDesPharmacies 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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-aicubic-dev-aiBot 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-aicubic-dev-aiBot 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
ContributorAuthor

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

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:masterAug 10, 2026
168 of 169 checks passed
@wing328wing328 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>
wing328 added a commit that referenced this pull request Aug 17, 2026
* [typescript-fetch] centralise date handling, add dateLibrary, fix format: date
Date handling was spread across four templates, each inlining its own
expression. That had three consequences:
1. `format: date` was not handled at all for form parameters, so a Date
was appended raw and stringified by the browser
("Wed Aug 05 2026 00:00:00 GMT+0200 (…)") instead of "2026-08-05".
2. `format: date` shifted by a day everywhere except UTC, in one direction
or the other. Parsing (`new Date('2026-08-05')`) and serialising
(`.toISOString().substring(0, 10)`) both work in UTC, but consumers do
not: a date picker builds local midnight and display reads local
getters. West of UTC a date from the API displays as the previous day;
east of UTC a locally built date is sent as the previous day. An RFC
3339 full-date has no offset, so both ends have to use the same wall
clock — they now both use the local calendar. `format: date-time` is a
genuine instant and stays UTC.
3. Whether dates were represented as Date or string was decided by
`withoutRuntimeChecks`, an unrelated flag about payload validation.
All call sites (models, oneOf models, path/query/form parameters and the
querystring helper) now route through serializeDate/serializeDateTime and
parseDate/parseDateTime in runtime.ts, so the representation is defined in
one place. The new `dateLibrary` option (`date`, the default and previous
behaviour, or `string`) makes the choice explicit; `withoutRuntimeChecks`
implies `string`, as before, since there is no model code left to convert
with.
Adds a spec fixture covering every location a date can appear in, two
sample builds (one per option value), and tests for the option, the
fallback and the serialisation semantics.
* [typescript-fetch] address review: ES6 target, year 0-99, invalid dates, unused imports
- serializeDate no longer uses padStart, which is ES2017: the es6-target
sample did not compile against its own tsconfig.
- parseDate builds the local date with setFullYear, so years 0000-0099 keep
their century instead of picking up the multi-argument Date constructor's
1900 offset ("0045-08-05" was parsed as 1945).
- parseDate rejects components that roll over, so an out-of-range date or a
day the local zone skipped returns Invalid Date rather than a plausible
wrong one. Previously "2026-13-45" became 2027-02-14.
- serializeDate throws RangeError on an invalid Date instead of emitting
"0NaN-NaN-NaN", matching serializeDateTime.
- Models without a date property no longer import the date helpers, via a
new x-hasDateVars extension mirroring the template's own branches. This
reverts most of the sample churn from the previous commit.
Drops testDateFormatUsesTheLocalCalendar: runtime.ts is identical for every
spec, so asserting its body only restated the template. The per-build
tsconfig typecheck covers the ES6 regression properly.
* [typescript-fetch] align the oneOf date guards with the date helpers
The oneOf branches tested a value with `new Date(json)` but converted it with
parseDate, so the two could disagree: "2026-02-30" passes the lenient test (V8
rolls it to March 2) and then parseDate rejects it, leaving the branch selected
and returning Invalid Date. Testing with the same helper that does the
conversion lets the oneOf fall through to another branch instead.
Adds a oneOf date member to the date-handling fixture. No sample in the repo
exercised these branches, so the generated form of the guard was invisible in
the samples; only the scalar variant is left out, because a scalar oneOf
primitive already fails `tsc --strict` on master (it can return undefined,
which is not in the union).
Drops the comment justifying the ES6-safe padding: the per-build tsconfig
typecheck already fails if someone reaches for padStart again.
* [typescript-fetch] rebase on master: fold the #24509 null guards into the date helpers
Upstream #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 #23935,
and regenerate the affected samples.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
* update TS samples
---------
Co-authored-by: Nicolas Medda <nicolas@lecomptoirdespharmacies.fr>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nicolas Medda <b2l.powa@gmail.com>
Co-authored-by: Esteban Gehring <esteban.gehring@gmail.com>
@jschonenberg

Copy link
Copy Markdown

@AntoineDuComptoirDesPharmacies thank you for your work!

Since splitOperationsByContentType was added as a global property, I would assume that it would automatically be available for all generators.
However, we are using the java generator and the property does not seem to be effective.

Is this as expected? Is it due to how the java generator is implemented?

Sign up for freeto 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

3 participants

@AntoineDuComptoirDesPharmacies@wing328@jschonenberg