Skip to content

fix(common): mark parse date pipe options as optional - #17780

Merged
kamilmysliwiec merged 1 commit into
nestjs:masterfrom
Chy-Zaber-Bin-Zahid:fix/parse-date-pipe-optional-options
Sep 17, 2026
Merged

kamilmysliwiec merged 1 commit into
nestjs:masterfrom
Chy-Zaber-Bin-Zahid:fix/parse-date-pipe-optional-options

Conversation

@Chy-Zaber-Bin-Zahid

@Chy-Zaber-Bin-Zahid Chy-Zaber-Bin-Zahid commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

ParseDatePipe is the only built-in pipe whose constructor argument is not decorated with @Optional():

// parse-int.pipe.ts, parse-bool.pipe.ts, parse-uuid.pipe.ts, parse-float.pipe.ts,
// parse-enum.pipe.ts, parse-array.pipe.ts, validation.pipe.ts …
constructor(@Optional() protected readonly options?: ParseIntPipeOptions)

// parse-date.pipe.ts
constructor(private readonly options: ParseDatePipeOptions = {})

The = {} default looks like it covers the missing-argument case, but it does not. The injector
never calls the constructor with a missing argument — it resolves constructor parameters from
design:paramtypes, which is Object for an interface, and then looks for a provider for that
token. Without the optional marker, resolution fails before the default value can ever apply.

So passing the pipe by class, the form the docs use for every other pipe, throws at bootstrap:

@Controller('date')
export class DateController {
  @Get()
  find(@Query('v', ParseDatePipe) v: Date) {
    return v;
  }
}
Error: Nest can't resolve dependencies of the ParseDatePipe (?).
Please make sure that the argument at index [0] is available in the current module.

Reproduction

Minimal repro against master — a single spec is enough, since the failure happens while the
module is being compiled:

import { Controller, Get, ParseBoolPipe, ParseDatePipe, Query } from '@nestjs/common';
import { Test } from '@nestjs/testing';

@Controller('bool')
class BoolController {
  @Get()
  find(@Query('v', ParseBoolPipe) v: boolean) {
    return v;
  }
}

@Controller('date')
class DateController {
  @Get()
  find(@Query('v', ParseDatePipe) v: Date) {
    return v;
  }
}

it('ParseBoolPipe compiles', async () => {
  await Test.createTestingModule({ controllers: [BoolController] }).compile(); // ✅ passes
});

it('ParseDatePipe compiles', async () => {
  await Test.createTestingModule({ controllers: [DateController] }).compile(); // ❌ throws
});

ParseBoolPipe compiles, ParseDatePipe does not. Identical usage, different outcome — so the
failure looks arbitrary from the outside, and the only workaround is to instantiate the pipe by
hand (@Query('v', new ParseDatePipe())).

The same underlying metadata gap is visible directly:

Reflect.getMetadata(OPTIONAL_DEPS_METADATA, ParseBoolPipe); // [0]
Reflect.getMetadata(OPTIONAL_DEPS_METADATA, ParseDatePipe); // undefined

What is the new behavior?

ParseDatePipe's options argument is decorated with @Optional(), so the injector passes
undefined and the existing = {} default applies. Passing the pipe by class now works exactly
like every other built-in pipe:

find(@Query('v', ParseDatePipe) v: Date) {} // ✅ resolves

private readonly options is also widened to protected readonly options, matching every other
pipe as well as this class's own exceptionFactory, so the pipe can be subclassed like its siblings.

Two regression tests were added to parse-date.pipe.spec.ts, following the existing
optional.decorator.spec.ts pattern:

  • the options argument is marked optional for the injector
  • constructing the pipe without options falls back to the defaults

Verified locally: the repro above fails on master and passes with this change, and the full unit
suite is green (280 files, 2949 tests) with npm run lint clean.

Does this PR introduce a breaking change?

  • Yes
  • No

Adding @Optional() only widens what the injector accepts, and privateprotected only widens
member visibility. Existing usage — including new ParseDatePipe({ ... }) and passing an instance
into a param decorator — is unaffected.

Other information

The fix is deliberately kept to the one pipe that is inconsistent; all other built-in pipes already
carry @Optional().

`ParseDatePipe` is the only built-in pipe whose constructor argument is not
decorated with `@Optional()`. The `= {}` default does not cover this, because
the injector never calls the constructor with a missing argument: it resolves
parameters from `design:paramtypes`, which is `Object` for an interface, and
then fails to find a provider for it.

As a result, passing the pipe by class — the form used throughout the docs for
every other pipe — throws at bootstrap:

    @get()
    find(@query('v', ParseDatePipe) v: Date) {}

    Nest can't resolve dependencies of the ParseDatePipe (?). Please make sure
    that the argument at index [0] is available in the current module.

`@Query('v', ParseBoolPipe)` works, so the failure looks arbitrary and the only
workaround is to instantiate the pipe manually (`new ParseDatePipe()`).

Decorate the argument with `@Optional()` so the injector passes `undefined` and
the existing default value applies. Also widen `private` to `protected` to match
every other pipe and this class's own `exceptionFactory`.
@kamilmysliwiec
kamilmysliwiec merged commit 2ddb8bd into nestjs:master Sep 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants