Skip to content

fix(schema): prioritize oneOf/anyOf/allOf over inferred type resolution#3781

Merged
kamilmysliwiec merged 1 commit into
nestjs:masterfrom
maruthang:fix/oneOf-references-object-3549
Apr 20, 2026
Merged

fix(schema): prioritize oneOf/anyOf/allOf over inferred type resolution#3781
kamilmysliwiec merged 1 commit into
nestjs:masterfrom
maruthang:fix/oneOf-references-object-3549

Conversation

@maruthang

Copy link
Copy Markdown
Contributor

PR Checklist

PR Type

  • Bugfix

What is the current behavior?

When using @ApiProperty({ oneOf: [...] }) on a property whose TypeScript type resolves to Object at runtime (e.g., union types like Example | Example[]), the generated swagger schema produces $ref: '#/components/schemas/Object' instead of the declared oneOf combinator.

This happens because createSchemaMetadata() resolves the inferred type before the combinator check in extractPropertiesFromType() has a chance to handle it. When the lazy type function name is not recognized (e.g., due to bundler/minifier transformations), the type falls through to createNotBuiltInTypeReference() which generates the incorrect $ref.

Issue Number: #3549

What is the new behavior?

When a schema combinator (oneOf, anyOf, allOf) is explicitly declared in the metadata, createSchemaMetadata() now returns early with the combinator intact, skipping type-based resolution entirely. This ensures the declared combinator always takes precedence over the inferred type.

Does this PR introduce a breaking change?

  • No

Other information

  • Added 3 unit tests covering: basic oneOf with Object type, oneOf with plugin metadata factory, and oneOf with broken lazy type function name
  • Added e2e test with oneOf using $ref to another schema (with and without plugin metadata)
  • All 161 unit tests and 102 e2e tests pass

@kamilmysliwiec

Copy link
Copy Markdown
Member

Could you resolve conflicts?

@maruthang maruthang force-pushed the fix/oneOf-references-object-3549 branch from c82482f to 6b9d420 Compare April 9, 2026 13:45
@maruthang

Copy link
Copy Markdown
Contributor Author

Hi @kamilmysliwiec, conflicts resolved! Rebased onto latest master and all 187 tests pass. Ready for review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes Swagger schema generation so explicitly-declared schema combinators (oneOf/anyOf/allOf) take precedence over inferred runtime types (notably Object from union types), preventing incorrect $ref: '#/components/schemas/Object' output (issue #3549).

Changes:

  • Short-circuit schema type resolution in SchemaObjectFactory.createSchemaMetadata() when a combinator is explicitly provided.
  • Add unit tests covering oneOf with Object design:type, plugin metadata factory behavior, and broken lazy type function names.
  • Add e2e coverage demonstrating oneOf with $ref to another schema (including plugin-metadata scenario) and update the generated api-spec.json.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
lib/services/schema-object-factory.ts Makes combinators take precedence by returning early before inferred-type resolution runs.
test/services/schema-object-factory.spec.ts Adds regression tests for oneOf + Object and broken lazy-type inference scenarios.
e2e/validate-schema.e2e-spec.ts Extends plugin metadata to cover the new oneOfWithRef field.
e2e/src/cats/classes/cat.class.ts Adds oneOfWithRef DTO property using $ref combinator and registers TagDto as an extra model.
e2e/api-spec.json Updates expected generated OpenAPI output to include oneOfWithRef.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/services/schema-object-factory.ts

// Manually set the metadata as the plugin would, with a broken lazy type
Reflect.defineMetadata(
'swagger/apiModelProperties',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — switched to DECORATORS.API_MODEL_PROPERTIES to match the rest of the spec file and stay resilient to any internal constant rename.

@kamilmysliwiec

Copy link
Copy Markdown
Member

@maruthang could you address copilot comments

When a schema combinator (oneOf, anyOf, allOf) is explicitly declared
via @ApiProperty, skip type-based resolution in createSchemaMetadata.
This prevents the inferred type (e.g., Object from union types) from
generating an incorrect $ref to '#/components/schemas/Object' instead
of honoring the declared combinator.

Closes nestjs#3549
@maruthang maruthang force-pushed the fix/oneOf-references-object-3549 branch from 6b9d420 to cc48bab Compare April 15, 2026 12:26
@maruthang

Copy link
Copy Markdown
Contributor Author

Addressed both Copilot comments @kamilmysliwiec:

  1. Array + combinator case (schema-object-factory.ts:683): removed the !metadata.isArray gate so declared combinators consistently short-circuit type-based resolution for array properties too. extractPropertiesFromType() already handles lifting combinators into items for arrays.
  2. Hard-coded metadata key (schema-object-factory.spec.ts:1007): replaced 'swagger/apiModelProperties' with DECORATORS.API_MODEL_PROPERTIES to match the rest of the file.

Also rebased onto latest master — merge conflict was in the unit-test spec between the new describe('inherited property type override') block (#3840) and my new describe('oneOf with Object type') block; both preserved side-by-side. All 36 unit tests pass locally.

@kamilmysliwiec kamilmysliwiec merged commit cc68f11 into nestjs:master Apr 20, 2026
1 check passed
@kamilmysliwiec

Copy link
Copy Markdown
Member

LGTM

@kamilmysliwiec

Copy link
Copy Markdown
Member

@maruthang had to revert this PR as it broke unit tests on master

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants