Skip to content

fix(type-helpers): apply deep partial substitution to plugin metadata - #4030

Merged
kamilmysliwiec merged 1 commit into
nestjs:masterfrom
kyungseopk1m:fix/deep-partial-type-plugin-metadata
Aug 17, 2026
Merged

fix(type-helpers): apply deep partial substitution to plugin metadata#4030
kamilmysliwiec merged 1 commit into
nestjs:masterfrom
kyungseopk1m:fix/deep-partial-type-plugin-metadata

Conversation

@kyungseopk1m

Copy link
Copy Markdown
Contributor

PR Checklist

PR Type

  • Bugfix

What is the current behavior?

Issue Number: N/A

DeepPartialType only walks properties that carry explicit @ApiProperty() metadata. For a DTO written with the CLI plugin, the properties live in the generated metadata factory instead, so two things go wrong:

  1. The plugin branch applies the optional validation decorator but never re-applies ApiProperty, so nested DTOs keep their original fully required type.
  2. isDtoClass decides whether a property type is a DTO by reading API_MODEL_PROPERTIES_ARRAY off the prototype. A plugin generated DTO has nothing there until the schema is explored, so nested plugin DTOs are not recognized as DTOs at all and the recursive wrapping is skipped.

The net effect is that DeepPartialType behaves like PartialType for anyone using the plugin, which is the recommended setup. Nested fields stay required.

applyMetadataFactory does not compensate for this. It applies the factory as ApiProperty, but it does not replace type: () => Inner with DeepPartialType(Inner).

What is the new behavior?

The plugin fields branch now applies ApiProperty eagerly with the same recursive type substitution used for explicitly decorated fields. This is the pattern #3822 introduced for PartialType, OmitType and PickType. DeepPartialType landed at almost the same time as that PR, so the two were in flight together and it never picked the pattern up.

isDtoClass also falls back to detecting the metadata factory, which is what actually enables the recursive wrapping. Reverting only that part while keeping the plugin fields branch still fails three of the new tests, and all three are the nested wrapping cases. Both changes are load bearing.

The fallback also fixes a second case that has nothing to do with the plugin branch: an explicit @ApiProperty({ type: () => SomePluginDto }) pointing at a plugin generated DTO now gets wrapped too.

While adding the fallback I hit a crash that already exists on master. If a lazy type factory throws, for example during a circular import, the catch hands the arrow function itself back as the resolved type. Arrow functions have no prototype, so getModelProperties dereferences undefined and reflect-metadata throws out of DeepPartialType at class definition time. That path was reachable through explicit decorators before this PR, and the plugin branch would have widened it, since the plugin emits type: () => X for every property. A prototype guard in isDtoClass closes both.

Tests cover the plugin only nested DTO, deep recursion through two levels, plugin declared arrays, the mixed case where one class has both explicit and plugin metadata, and the throwing factory on both routes. The existing explicit decorator tests are unchanged and keep passing, which is the control.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Full suite passes at 412 tests. Reverting the source while keeping the new tests fails 5 of them.

@kamilmysliwiec
kamilmysliwiec merged commit fccfa59 into nestjs:master Aug 17, 2026
1 check passed
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.

2 participants