Skip to content

Handle required-only allOf constraints - #2850

Open
darkbasic wants to merge 3 commits into
openapi-ts:mainfrom
darkbasic:fix/required-only-allof
Open

Handle required-only allOf constraints#2850
darkbasic wants to merge 3 commits into
openapi-ts:mainfrom
darkbasic:fix/required-only-allof

Conversation

@darkbasic

Copy link
Copy Markdown
Contributor

Summary

Handle allOf members that only make existing object properties required.

Fixes #1474
Related to #1520 and #2570

Problem

Given a schema such as:

allOf:
  - $ref: "#/components/schemas/Base"
  - type: object
    required: [name]

openapi-typescript generated Base & Record<string, never>. The second member is a presence constraint, but it was transformed as an empty object, making the resulting type effectively unusable.

Solution

Exact { required: [...] } and { type: object, required: [...] } members are translated through the existing parent required flow. For referenced schemas this produces WithRequired<Base, "name"> instead of an empty-object intersection.

The implementation is intentionally conservative:

  • Every required key must exist in the retained object schemas.
  • Typed constraints require another retained, non-nullable object assertion.
  • Schemas with extra keywords, callbacks, renamed or filtered properties, unions, nullability, or early-return types keep the previous behavior.
  • Recursive allOf references are followed with cycle protection.
  • Existing discriminator handling is preserved while allowing additional non-discriminator fields to become required.

The main logic is in transform/schema-object.ts. The composition tests include generated TypeScript compilation checks, and the DigitalOcean fixture was regenerated because it contains several real examples of this schema pattern.

Validation

  • 324 package tests passed
  • TypeScript and generated-example checks passed
  • Build and package export validation passed
  • DigitalOcean generated fixture compiles

Notes

While the original implementation has been written by me, after several rounds of validation by GPT-5.6 Sol it found so many corner cases that it basically rewrote it from scratch.

Translate exact required-only allOf members through the existing required-property flow while preserving unsafe, annotated, polymorphic, and transformed schemas.

Fixes openapi-ts#1474

Related to openapi-ts#1520 and openapi-ts#2570
@darkbasic
darkbasic requested a review from a team as a code owner July 31, 2026 08:30
@darkbasic
darkbasic requested a review from drwpow July 31, 2026 08:30
@netlify

netlify Bot commented Jul 31, 2026

Copy link
Copy Markdown

👷 Deploy request for openapi-ts pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 89e7c70

@changeset-bot

changeset-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 89e7c70

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
openapi-typescript Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@darkbasic

darkbasic commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up

After opening the PR, I tested the package with a generator that always supplies a custom transform callback. Even though the callback only converts binary schemas to Blob, its presence disabled the original required-only allOf translation for the entire document.

Commit f82b194c adds transform-aware handling. It preserves callback order and replacement behavior, then applies untouched typed object constraints to the completed allOf intersection through WithRequiredObject<T, K>. Referenced components still use their final transformed types; if a callback removes a required property, that property remains required as unknown instead of producing invalid TypeScript.

This keeps transformed output concise:

WithRequiredObject<CompanyResource, "organization" | "addresses" | "processingTypes">

The implementation deliberately keeps WithRequired unchanged because parent-level required and an explicit type: object constraint have different semantics. Callback replacements of the required-only member remain authoritative, and helper-name conflicts use an equivalent anonymous fallback.

The first transform-aware version was much larger than necessary. Commit 89e7c703 keeps the same behavior while:

  • sharing one canonical AST between the named helper and collision fallback;
  • consolidating repeated callback and compiler-test infrastructure;
  • replacing one-off tests with focused behavior matrices;
  • retaining coverage for recursion, unsafe schemas, callback lifecycle, replacements, discriminators, collisions, and generated TypeScript semantics.

That reduced the PR from 2,890 additions to 1,436 additions without dropping supported behavior.

The final suite passes with 306 tests. TypeScript checks, generated examples, build and package exports, the DigitalOcean fixture, live schema generation, and a freshly packed consumer all pass. The live callback-enabled schema produces three concise helper references, no Record<string, never> intersections, and no TypeScript diagnostics.

Notes

The follow-up commit has been done entirely by GPT-5.6 Sol.

@darkbasic
darkbasic marked this pull request as draft August 3, 2026 11:56
Apply typed required-only constraints to the completed transformed intersection while preserving callback replacements and legacy WithRequired behavior.
Consolidate helper generation and reduce duplicated tests while preserving transform support and collision-safe output.
@darkbasic
darkbasic force-pushed the fix/required-only-allof branch from 6c81ae2 to 89e7c70 Compare August 4, 2026 10:10
@darkbasic
darkbasic marked this pull request as ready for review August 4, 2026 10:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

allOff with required returning Record<string, never>

1 participant