zod-nest 1.8.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,6 +81,7 @@ export class UserDto extends createZodDto(userSchema) {}
81
81
  // users.controller.ts
82
82
  import { Body, Controller, Get, Post } from '@nestjs/common';
83
83
  import { ZodResponse } from 'zod-nest';
84
+
84
85
  import { UserDto } from './user.dto';
85
86
 
86
87
  @Controller('users')
@@ -104,6 +105,7 @@ export class UsersController {
104
105
  // app.module.ts
105
106
  import { Module } from '@nestjs/common';
106
107
  import { ZodNestModule } from 'zod-nest';
108
+
107
109
  import { UsersController } from './users.controller';
108
110
 
109
111
  @Module({
@@ -118,6 +120,7 @@ export class AppModule {}
118
120
  import { NestFactory } from '@nestjs/core';
119
121
  import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
120
122
  import { applyZodNest } from 'zod-nest';
123
+
121
124
  import { AppModule } from './app.module';
122
125
 
123
126
  async function bootstrap() {
@@ -148,11 +151,13 @@ Every DTO is one Zod schema wrapped in a class. The class exists so NestJS' intr
148
151
 
149
152
  The class returned by `createZodDto(schema)` carries `schema`, `id`, `io: 'input'`, and a lazy `Output` sibling. `parse` / `safeParse` are static methods on the class. The class is tagged with `Symbol.for('zod-nest.dto')` so `ZodValidationPipe` and `ZodSerializerInterceptor` can discriminate it from plain constructors. The id comes from `schema.meta({ id })` when present (preferred) or from the second-argument options.
150
153
 
154
+ **Naming is exposing.** Every schema put through `registerSchema()` — directly, or transitively via `createZodDto` / `@ZodBody` / `extend` / descendant discovery — lands in `components.schemas` when `applyZodNest` runs, regardless of whether any `$ref` in the doc points at it. If you give a schema `.meta({ id })`, you're declaring it documented. Anonymous schemas without an id stay inlined where used.
155
+
151
156
  See [`docs/dto.md`](docs/dto.md) for the full surface.
152
157
 
153
158
  ### Schemas that don't fit a class
154
159
 
155
- A schema whose `z.infer<>` is a TypeScript union — `z.union`, `z.discriminatedUnion`, or `z.intersection(obj, union)` — can't be used as a class base because TS rejects unions as constructor return types (TS2509: *Base constructor return type ... is not an object type*). For these, skip `createZodDto` and pair the raw schema with parameter-level decorators that handle OpenAPI emission directly:
160
+ A schema whose `z.infer<>` is a TypeScript union — `z.union`, `z.discriminatedUnion`, or `z.intersection(obj, union)` — can't be used as a class base because TS rejects unions as constructor return types (TS2509: _Base constructor return type ... is not an object type_). For these, skip `createZodDto` and pair the raw schema with parameter-level decorators that handle OpenAPI emission directly:
156
161
 
157
162
  ```ts
158
163
  const IntersectionWithUnion = z
@@ -205,6 +210,18 @@ At request time, `ZodSerializerInterceptor` looks at `response.statusCode` and p
205
210
 
206
211
  See [`docs/responses.md`](docs/responses.md) for the precedence chain and `passthroughOnError`.
207
212
 
213
+ ### Streaming responses (SSE, NDJSON, binary)
214
+
215
+ Streamed bodies are written straight to the response buffer — they shouldn't be validated, and the OpenAPI media type isn't `application/json`. Pass `contentType` (or declare a stream-typed `@Header('Content-Type', …)`) and `@ZodResponse` documents the response under that media type and skips validation:
216
+
217
+ ```ts
218
+ @Sse('events')
219
+ @ZodResponse({ type: NotificationEventDto, contentType: 'text/event-stream' })
220
+ streamEvents(): Observable<MessageEvent> { /* … */ }
221
+ ```
222
+
223
+ `stream` is inferred `true` for known stream types (`text/event-stream`, `application/x-ndjson`, `application/octet-stream`, `application/pdf`, `image/*`, `audio/*`, `video/*`); override with `stream: false`, or extend the set globally via `ZodNestModule.forRoot({ streamContentTypes: [...] })`. See [`docs/recipes/streaming-responses.md`](docs/recipes/streaming-responses.md).
224
+
208
225
  ## Usage
209
226
 
210
227
  ### Creating DTOs
@@ -226,12 +243,18 @@ The id is what appears as the `components.schemas` key in the OpenAPI document a
226
243
 
227
244
  ```ts
228
245
  // Preferred — on the schema, via Zod's metadata
229
- const userSchema = z.object({ /* ... */ }).meta({ id: 'User' });
246
+ const userSchema = z
247
+ .object({
248
+ /* ... */
249
+ })
250
+ .meta({ id: 'User' });
230
251
  class UserDto extends createZodDto(userSchema) {}
231
252
 
232
253
  // Also valid — passed through createZodDto's options
233
254
  class UserDto extends createZodDto(
234
- z.object({ /* ... */ }),
255
+ z.object({
256
+ /* ... */
257
+ }),
235
258
  { id: 'User' },
236
259
  ) {}
237
260
  ```
@@ -243,8 +266,8 @@ If you pass neither, the class name is used as a fallback. Under minification
243
266
  ### Input validation
244
267
 
245
268
  ```ts
246
- import { ZodValidationPipe } from 'zod-nest';
247
269
  import { APP_PIPE } from '@nestjs/core';
270
+ import { ZodValidationPipe } from 'zod-nest';
248
271
 
249
272
  @Controller('things')
250
273
  class ThingsController {
@@ -308,14 +331,14 @@ Stack `@ZodResponse` per status code:
308
331
  import { Get, HttpStatus } from '@nestjs/common';
309
332
  import { ZodResponse } from 'zod-nest';
310
333
 
311
- class UserDto extends createZodDto(z.object({ id: z.string() }), { id: 'User' }) {}
312
- class ErrorDto extends createZodDto(z.object({ code: z.number() }), { id: 'Error' }) {}
334
+ class UserDto extends createZodDto(z.object({ id: z.string() }), { id: 'User' }) {}
335
+ class ErrorDto extends createZodDto(z.object({ code: z.number() }), { id: 'Error' }) {}
313
336
  class FatalDto extends createZodDto(z.object({ trace: z.string() }), { id: 'Fatal' }) {}
314
337
 
315
338
  class UsersController {
316
339
  @Get(':id')
317
- @ZodResponse({ type: UserDto }) // 200 inferred
318
- @ZodResponse({ status: HttpStatus.NOT_FOUND, type: ErrorDto })
340
+ @ZodResponse({ type: UserDto }) // 200 inferred
341
+ @ZodResponse({ status: HttpStatus.NOT_FOUND, type: ErrorDto })
319
342
  @ZodResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, type: FatalDto })
320
343
  getUser(): void {}
321
344
  }
@@ -370,8 +393,7 @@ import { ZodNestModule } from 'zod-nest';
370
393
  ZodNestModule.forRoot({
371
394
  validationLogs: { input: true, output: true },
372
395
  redactKeys: ['password', 'token', 'sessionId'],
373
- createSerializationException: (err, ctx) =>
374
- new MyCustomFiveHundred(err, ctx),
396
+ createSerializationException: (err, ctx) => new MyCustomFiveHundred(err, ctx),
375
397
  }),
376
398
  ],
377
399
  controllers: [UsersController],
@@ -383,14 +405,14 @@ class AppModule {}
383
405
 
384
406
  ## Module options
385
407
 
386
- | Option | Type | Default | What it does |
387
- |---|---|---|---|
388
- | `createValidationException` | `(err, argMetadata) => unknown` | uses `ZodValidationException` | Custom 400 exception for input failures |
408
+ | Option | Type | Default | What it does |
409
+ | ------------------------------ | ------------------------------------ | -------------------------------- | ----------------------------------------------------------- |
410
+ | `createValidationException` | `(err, argMetadata) => unknown` | uses `ZodValidationException` | Custom 400 exception for input failures |
389
411
  | `createSerializationException` | `(err, executionContext) => unknown` | uses `ZodSerializationException` | Custom 500 exception for output failures (strict mode only) |
390
- | `validationLogs` | `boolean \| { input?, output? }` | `false` | Opt-in failure-only logging |
391
- | `logger` | `LoggerService` | NestJS `Logger` | Replace the logger (pino, winston, …) |
392
- | `redactKeys` | `readonly string[]` | `DEFAULT_REDACT_KEYS` | Keys redacted in logs (replaces default list, no merge) |
393
- | `maxLoggedValueBytes` | `number` | `4096` | Truncate oversize logged values |
412
+ | `validationLogs` | `boolean \| { input?, output? }` | `false` | Opt-in failure-only logging |
413
+ | `logger` | `LoggerService` | NestJS `Logger` | Replace the logger (pino, winston, …) |
414
+ | `redactKeys` | `readonly string[]` | `DEFAULT_REDACT_KEYS` | Keys redacted in logs (replaces default list, no merge) |
415
+ | `maxLoggedValueBytes` | `number` | `4096` | Truncate oversize logged values |
394
416
 
395
417
  `DEFAULT_REDACT_KEYS` includes `password`, `secret`, `apiKey`, `authorization`, `bearer`, `token`, `accessToken`, `refreshToken`, `jwt`, `cookie`, `set-cookie`. Matching is case-insensitive and applied at any depth in the logged value.
396
418
 
@@ -410,7 +432,7 @@ A log entry carries:
410
432
 
411
433
  Redaction is **case-insensitive** at any depth — a key named `Password` deep in a nested object is replaced with `'[REDACTED]'` just like a top-level `password`. Truncation replaces values larger than `maxLoggedValueBytes` (UTF-8 bytes) with `{ _truncated: true, _originalBytes, _preview }` so you keep enough context to debug without flooding the logger.
412
434
 
413
- Supplying `redactKeys` **replaces** the default list — there is no merge. If you want to *add* keys, spread `DEFAULT_REDACT_KEYS`:
435
+ Supplying `redactKeys` **replaces** the default list — there is no merge. If you want to _add_ keys, spread `DEFAULT_REDACT_KEYS`:
414
436
 
415
437
  ```ts
416
438
  import { DEFAULT_REDACT_KEYS, ZodNestModule } from 'zod-nest';
@@ -448,27 +470,35 @@ See [`docs/composition.md`](docs/composition.md) for the full contract, current
448
470
  A compact, link-out index. Type signatures and detailed semantics live in the companion docs.
449
471
 
450
472
  **DTO** — [`docs/dto.md`](docs/dto.md)
473
+
451
474
  - `createZodDto(schema, options?)`, `isZodDto(value)`, `ZodDto<TSchema>`, `Io`
452
475
 
453
476
  **Validation** — [`docs/validation-pipe.md`](docs/validation-pipe.md)
477
+
454
478
  - `ZodValidationPipe`, `ZodValidationException`, `ZodValidationPipeOptions`, `CreateValidationException`
455
479
 
456
480
  **Response** — [`docs/responses.md`](docs/responses.md)
481
+
457
482
  - `@ZodResponse({ status?, type, description?, passthroughOnError? })`, `ZodSerializerInterceptor`, `ZodSerializationException`, `defaultStatusFor`, `resolveEffectiveStatus`, `ResponseStatusInput`, `ResponseStatusWildcard`, `ResponseVariant`, `ZOD_RESPONSES_METADATA_KEY`
458
483
 
459
484
  **Parameter decorators for raw schemas** — [`docs/recipes/intersection-with-union.md`](docs/recipes/intersection-with-union.md)
485
+
460
486
  - `@ZodBody(schema, options?)`, `@ZodQuery(schema, options?)`, `@ZodHeaders(schema, options?)`, `@ZodCookies(schema, options?)`, `ZodBodyOptions`, `ZodQueryOptions`, `ZodHeadersOptions`, `ZodCookiesOptions`
461
487
 
462
488
  **Document** — [`docs/swagger-integration.md`](docs/swagger-integration.md)
489
+
463
490
  - `applyZodNest(rawDoc, options)`, `ApplyZodNestOptions`, `ZodNestDocumentError`
464
491
 
465
492
  **Module** — [`docs/module-options.md`](docs/module-options.md) / [`docs/logging.md`](docs/logging.md)
493
+
466
494
  - `ZodNestModule.forRoot(options?)`, `ZodNestModuleOptions`, `DEFAULT_REDACT_KEYS`, `DEFAULT_MAX_LOGGED_VALUE_BYTES`, `ZOD_NEST_OPTIONS`
467
495
 
468
496
  **Schema engine** — single-schema mode and extension points
497
+
469
498
  - `toOpenApi(schema, opts)`, `createRegistry()`, `defaultRegistry`, `registerSchema(schema, registry?, options?)`, `ZodNestRegistry`, `RegisterSchemaOptions`, `Override`, `OverrideContext`, `overrideJSONSchema(schema, fragment | { input?, output? })`, `OverrideJSONSchemaArg`, `ZodNestError`, `ZodNestUnrepresentableError`, `extend`, `getLineage`, `LineageEntry`
470
499
 
471
500
  **Helpers** (subpath: `zod-nest/helpers`) — common JSON Schema fragments + presets for assembling overrides
501
+
472
502
  - **Fragment catalog** (frozen consts): `dateTimeFragment`, `dateFragment`, `timeFragment`, `uuidFragment`, `emailFragment`, `uriFragment`, `hostnameFragment`, `ipv4Fragment`, `ipv6Fragment`, `binaryFragment`, `byteFragment`, `int32Fragment`, `int64Fragment`, `floatFragment`, `doubleFragment`, `opaqueFragment`
473
503
  - **Sugar functions**: `binary(opts?)`, `opaque(opts?)`
474
504
  - **Type-strict composition**: `enrich(base, extras)` — extras are typed per fragment family
@@ -478,31 +508,31 @@ See [`docs/recipes/custom-openapi-overrides.md`](docs/recipes/custom-openapi-ove
478
508
 
479
509
  ## Documentation
480
510
 
481
- | Topic | Doc |
482
- |---|---|
483
- | Why this library exists | [`docs/why-this-exists.md`](docs/why-this-exists.md) |
484
- | `createZodDto` in depth | [`docs/dto.md`](docs/dto.md) |
485
- | Input validation | [`docs/validation-pipe.md`](docs/validation-pipe.md) |
486
- | Responses, multi-status, status resolution | [`docs/responses.md`](docs/responses.md) |
487
- | Module options reference | [`docs/module-options.md`](docs/module-options.md) |
488
- | Validation logging | [`docs/logging.md`](docs/logging.md) |
489
- | Swagger integration & custom emission | [`docs/swagger-integration.md`](docs/swagger-integration.md) |
490
- | Composition (experimental) | [`docs/composition.md`](docs/composition.md) |
491
- | Exception classes | [`docs/exceptions.md`](docs/exceptions.md) |
492
- | Recipes | [`docs/recipes/`](docs/recipes/) |
511
+ | Topic | Doc |
512
+ | ------------------------------------------ | ------------------------------------------------------------ |
513
+ | Why this library exists | [`docs/why-this-exists.md`](docs/why-this-exists.md) |
514
+ | `createZodDto` in depth | [`docs/dto.md`](docs/dto.md) |
515
+ | Input validation | [`docs/validation-pipe.md`](docs/validation-pipe.md) |
516
+ | Responses, multi-status, status resolution | [`docs/responses.md`](docs/responses.md) |
517
+ | Module options reference | [`docs/module-options.md`](docs/module-options.md) |
518
+ | Validation logging | [`docs/logging.md`](docs/logging.md) |
519
+ | Swagger integration & custom emission | [`docs/swagger-integration.md`](docs/swagger-integration.md) |
520
+ | Composition (experimental) | [`docs/composition.md`](docs/composition.md) |
521
+ | Exception classes | [`docs/exceptions.md`](docs/exceptions.md) |
522
+ | Recipes | [`docs/recipes/`](docs/recipes/) |
493
523
 
494
524
  ## Compatibility matrix
495
525
 
496
526
  `zod-nest` declares explicit min + max peer-dep ranges in `package.json`: `zod >=4.4.0 <5.0.0`, `@nestjs/common >=11.0.1 <12.0.0`, `@nestjs/core >=11.0.1 <12.0.0`, `@nestjs/swagger >=11.0.0 <12.0.0`, `rxjs >=7.6.0 <8.0.0`, `reflect-metadata >=0.2.0 <0.3.0`, Node `>=22`. CI validates those claims by running the full test suite against the cells below; a red cell is a real blocker. Upper bounds are deliberate — a new peer major has to land in a real PR with a `/check-upstream-updates` audit before consumers can install it against this library.
497
527
 
498
- | Cell | `zod` | `@nestjs/common` | `@nestjs/core` | `@nestjs/swagger` | `rxjs` | `reflect-metadata` |
499
- |---|---|---|---|---|---|---|
500
- | `floor` | 4.4.0 | 11.0.1 | 11.0.1 | 11.0.0 | 7.6.0 | 0.2.0 |
501
- | `zod-latest` | latest | 11.0.1 | 11.0.1 | 11.0.0 | 7.6.0 | 0.2.0 |
502
- | `nest-latest` | 4.4.0 | latest | latest | latest | 7.6.0 | 0.2.0 |
503
- | `rxjs-latest` | 4.4.0 | 11.0.1 | 11.0.1 | 11.0.0 | latest | 0.2.0 |
504
- | `reflect-metadata-latest` | 4.4.0 | 11.0.1 | 11.0.1 | 11.0.0 | 7.6.0 | latest |
505
- | `all-latest` | latest | latest | latest | latest | latest | latest |
528
+ | Cell | `zod` | `@nestjs/common` | `@nestjs/core` | `@nestjs/swagger` | `rxjs` | `reflect-metadata` |
529
+ | ------------------------- | ------ | ---------------- | -------------- | ----------------- | ------ | ------------------ |
530
+ | `floor` | 4.4.0 | 11.0.1 | 11.0.1 | 11.0.0 | 7.6.0 | 0.2.0 |
531
+ | `zod-latest` | latest | 11.0.1 | 11.0.1 | 11.0.0 | 7.6.0 | 0.2.0 |
532
+ | `nest-latest` | 4.4.0 | latest | latest | latest | 7.6.0 | 0.2.0 |
533
+ | `rxjs-latest` | 4.4.0 | 11.0.1 | 11.0.1 | 11.0.0 | latest | 0.2.0 |
534
+ | `reflect-metadata-latest` | 4.4.0 | 11.0.1 | 11.0.1 | 11.0.0 | 7.6.0 | latest |
535
+ | `all-latest` | latest | latest | latest | latest | latest | latest |
506
536
 
507
537
  Cell definitions live in [`.github/compat-matrix.json`](.github/compat-matrix.json). The CI workflow ([`.github/workflows/compat-matrix.yml`](.github/workflows/compat-matrix.yml)) runs on every push to `main` and weekly on Monday — when a cell fails, the workflow opens (or comments on) a GitHub issue labelled `compat-matrix-failure` so the regression is tracked outside the Actions UI. Editing the JSON is the formal way to extend or shrink supported ranges. Node is not matrixed — the `>=22` floor is enforced by `engines`.
508
538
 
package/dist/index.d.mts CHANGED
@@ -292,6 +292,93 @@ interface ValidationLogContext {
292
292
  }
293
293
  type LogValidationFailure = (err: z.ZodError, value: unknown, ctx: ValidationLogContext) => void;
294
294
 
295
+ /**
296
+ * Public metadata key for the array of `ResponseVariant` records attached
297
+ * to handler methods by `@ZodResponse(...)`. Symbol.for() so external
298
+ * consumers (e.g. custom interceptors or doc builders) can read the same
299
+ * registry across realms.
300
+ */
301
+ declare const ZOD_RESPONSES_METADATA_KEY: unique symbol;
302
+ type ResponseVariantKind = 'single' | 'array' | 'tuple';
303
+ /**
304
+ * OpenAPI 3.1 range keys accepted by `@ZodResponse({ status })`. Matched
305
+ * by `ZodSerializerInterceptor` after exact numeric matches fail —
306
+ * `'2XX'` covers 200–299, `'4XX'` covers 400–499, and so on.
307
+ */
308
+ type ResponseStatusWildcard = '1XX' | '2XX' | '3XX' | '4XX' | '5XX';
309
+ /**
310
+ * Description payload accepted by `@ZodResponse(...)` and passed through to
311
+ * `applyZodNest`'s `@ApiResponse(...)` emitter. String form is shorthand for
312
+ * `{ description }`; the object form lets users declare OpenAPI response
313
+ * `headers` / `links` alongside the description.
314
+ */
315
+ type ZodResponseDescription = string | {
316
+ description: string;
317
+ headers?: Record<string, unknown>;
318
+ links?: Record<string, unknown>;
319
+ };
320
+ /**
321
+ * One variant record per `@ZodResponse(...)` call. `dto` is kept alongside
322
+ * `validationSchema` so `applyZodNest` can emit `@ApiResponse({ type })`
323
+ * without unwrapping the runtime-only `z.array(...)` / `z.tuple([...])` wrapper.
324
+ *
325
+ * `status` is `undefined` when the user didn't pass one explicitly OR when
326
+ * the user wrote `'default'` (the decorator collapses both to `undefined` —
327
+ * they mean the same thing: "resolve to the method's default status at
328
+ * request time"). The effective status is resolved lazily by
329
+ * `resolveEffectiveStatus(variant, handler)` because `@ZodResponse` runs
330
+ * *before* NestJS' route decorators (`@Get`, `@Post`, ...) under
331
+ * TypeScript's bottom-up application order, so `METHOD_METADATA` is not
332
+ * yet set when the decorator evaluates.
333
+ *
334
+ * A wildcard string (`'2XX'` / `'4XX'` / ...) is kept verbatim — the
335
+ * matcher in `ZodSerializerInterceptor` handles range comparison.
336
+ */
337
+ interface ResponseVariant {
338
+ status: number | ResponseStatusWildcard | undefined;
339
+ kind: ResponseVariantKind;
340
+ dto: ZodDto | readonly ZodDto[];
341
+ validationSchema: z.ZodType;
342
+ description?: ZodResponseDescription;
343
+ passthroughOnError: boolean;
344
+ /**
345
+ * Raw `contentType` option from `@ZodResponse`, if set. `undefined` means
346
+ * "resolve lazily" — `resolveContentType` falls back to a stream-typed
347
+ * `@Header('Content-Type', …)` or `application/json`. Drives the OpenAPI
348
+ * media-type key.
349
+ */
350
+ contentType?: string;
351
+ /**
352
+ * Raw `stream` option from `@ZodResponse`, if set. `undefined` means
353
+ * "infer" — `isStreamResponse` treats the response as a stream when the
354
+ * effective content type is a known stream type. Streams skip validation.
355
+ */
356
+ stream?: boolean;
357
+ }
358
+
359
+ /**
360
+ * Content types whose bodies zod-nest treats as streams by default: the
361
+ * handler writes them straight to the response buffer (SSE, NDJSON, raw
362
+ * binary, files), so `ZodSerializerInterceptor` skips validation and the
363
+ * OpenAPI media-type key becomes the stream type rather than `application/json`.
364
+ *
365
+ * A trailing `/*` marks a family prefix — `image/*` matches `image/png`,
366
+ * `image/jpeg`, … — everything else matches exactly. Consumers extend this set
367
+ * *additively* via `ZodNestModuleOptions.streamContentTypes`; the defaults are
368
+ * always retained so SSE / NDJSON detection can't be accidentally dropped.
369
+ */
370
+ declare const DEFAULT_STREAM_CONTENT_TYPES: readonly string[];
371
+ /**
372
+ * Normalised stream-content-type matcher: exact media types in a `Set` for
373
+ * O(1) lookup, family prefixes (`image/`, …) checked with `startsWith`. Built
374
+ * once by `normalizeStreamMatcher` from the default list (decoration time) or
375
+ * the default ∪ module-configured list (runtime).
376
+ */
377
+ interface StreamContentTypeMatcher {
378
+ exact: ReadonlySet<string>;
379
+ prefixes: readonly string[];
380
+ }
381
+
295
382
  /**
296
383
  * Module-scope factory for the exception thrown by `ZodValidationPipe` on
297
384
  * input validation failure. Mirrors the existing per-pipe option but lives
@@ -329,6 +416,14 @@ interface ZodNestModuleOptions {
329
416
  * values become `{ _truncated: true, _originalBytes, _preview }`.
330
417
  */
331
418
  maxLoggedValueBytes?: number;
419
+ /**
420
+ * Additional response content types treated as streams — written directly
421
+ * to the response buffer and never validated by `ZodSerializerInterceptor`.
422
+ * MERGED with the built-in `DEFAULT_STREAM_CONTENT_TYPES` (SSE, NDJSON,
423
+ * octet-stream, pdf, `image/*`, `audio/*`, `video/*`), so the defaults are
424
+ * always retained. A trailing `/*` entry matches a media-type family.
425
+ */
426
+ streamContentTypes?: readonly string[];
332
427
  }
333
428
  /**
334
429
  * Resolved options consumed by pipe + interceptor. `forRoot()` builds this
@@ -342,6 +437,8 @@ interface NormalizedZodNestOptions {
342
437
  logInputFailure: LogValidationFailure;
343
438
  /** No-op when `validationLogs.output` resolved to false. */
344
439
  logOutputFailure: LogValidationFailure;
440
+ /** Built-in defaults ∪ `streamContentTypes`, used by the interceptor's stream check. */
441
+ streamMatcher: StreamContentTypeMatcher;
345
442
  }
346
443
  declare const DEFAULT_REDACT_KEYS: readonly string[];
347
444
  declare const DEFAULT_MAX_LOGGED_VALUE_BYTES = 4096;
@@ -379,57 +476,6 @@ declare class ZodValidationPipe implements PipeTransform {
379
476
  private static parseArg;
380
477
  }
381
478
 
382
- /**
383
- * Public metadata key for the array of `ResponseVariant` records attached
384
- * to handler methods by `@ZodResponse(...)`. Symbol.for() so external
385
- * consumers (e.g. custom interceptors or doc builders) can read the same
386
- * registry across realms.
387
- */
388
- declare const ZOD_RESPONSES_METADATA_KEY: unique symbol;
389
- type ResponseVariantKind = 'single' | 'array' | 'tuple';
390
- /**
391
- * OpenAPI 3.1 range keys accepted by `@ZodResponse({ status })`. Matched
392
- * by `ZodSerializerInterceptor` after exact numeric matches fail —
393
- * `'2XX'` covers 200–299, `'4XX'` covers 400–499, and so on.
394
- */
395
- type ResponseStatusWildcard = '1XX' | '2XX' | '3XX' | '4XX' | '5XX';
396
- /**
397
- * Description payload accepted by `@ZodResponse(...)` and passed through to
398
- * `applyZodNest`'s `@ApiResponse(...)` emitter. String form is shorthand for
399
- * `{ description }`; the object form lets users declare OpenAPI response
400
- * `headers` / `links` alongside the description.
401
- */
402
- type ZodResponseDescription = string | {
403
- description: string;
404
- headers?: Record<string, unknown>;
405
- links?: Record<string, unknown>;
406
- };
407
- /**
408
- * One variant record per `@ZodResponse(...)` call. `dto` is kept alongside
409
- * `validationSchema` so `applyZodNest` can emit `@ApiResponse({ type })`
410
- * without unwrapping the runtime-only `z.array(...)` / `z.tuple([...])` wrapper.
411
- *
412
- * `status` is `undefined` when the user didn't pass one explicitly OR when
413
- * the user wrote `'default'` (the decorator collapses both to `undefined` —
414
- * they mean the same thing: "resolve to the method's default status at
415
- * request time"). The effective status is resolved lazily by
416
- * `resolveEffectiveStatus(variant, handler)` because `@ZodResponse` runs
417
- * *before* NestJS' route decorators (`@Get`, `@Post`, ...) under
418
- * TypeScript's bottom-up application order, so `METHOD_METADATA` is not
419
- * yet set when the decorator evaluates.
420
- *
421
- * A wildcard string (`'2XX'` / `'4XX'` / ...) is kept verbatim — the
422
- * matcher in `ZodSerializerInterceptor` handles range comparison.
423
- */
424
- interface ResponseVariant {
425
- status: number | ResponseStatusWildcard | undefined;
426
- kind: ResponseVariantKind;
427
- dto: ZodDto | readonly ZodDto[];
428
- validationSchema: z.ZodType;
429
- description?: ZodResponseDescription;
430
- passthroughOnError: boolean;
431
- }
432
-
433
479
  /**
434
480
  * Accepted shapes for `@ZodResponse({ type })`:
435
481
  * - `Dto` → validates as `Dto.schema` (single-DTO response).
@@ -462,6 +508,20 @@ interface ZodResponseOptions {
462
508
  type: ZodResponseType;
463
509
  description?: ZodResponseDescription;
464
510
  passthroughOnError?: boolean;
511
+ /**
512
+ * Response content type, used as the OpenAPI media-type key (`@ApiResponse`
513
+ * `content`). Defaults to `'application/json'`. Set a stream type
514
+ * (`'text/event-stream'`, `'application/x-ndjson'`, …) to document a streamed
515
+ * body; combined with `stream` it also turns off response validation.
516
+ */
517
+ contentType?: string;
518
+ /**
519
+ * When `true`, the response is written directly to the buffer and
520
+ * `ZodSerializerInterceptor` skips validation. When unset, it is inferred:
521
+ * a stream-typed `contentType` (or a stream-typed `@Header('Content-Type', …)`)
522
+ * implies `true`. See `DEFAULT_STREAM_CONTENT_TYPES`.
523
+ */
524
+ stream?: boolean;
465
525
  }
466
526
  /**
467
527
  * Method-only decorator. Declares a typed response variant for the handler
@@ -474,16 +534,16 @@ interface ZodResponseOptions {
474
534
  * `'NXX'` wildcard). The wrapped Zod schema (array / tuple) is built once
475
535
  * at decoration time — no per-request schema construction.
476
536
  *
477
- * **Decorator-ordering note (implicit `status` only).** TypeScript decorators
478
- * apply bottom-up, so `@ZodResponse` (typically written above `@Get` /
479
- * `@HttpCode`) executes its factory body *first* before sibling
480
- * decorators have written their `HTTP_CODE_METADATA` / `METHOD_METADATA`.
481
- * When `opts.status` is explicit, the `@ApiResponse(...)` call is applied
482
- * synchronously there's nothing to wait for. When `opts.status` is
483
- * implicit (resolves to `@HttpCode` or the HTTP-method default), the
484
- * `@ApiResponse(...)` call is deferred via `queueMicrotask` so the sibling
485
- * metadata has settled by the time we read it. See `docs/responses.md →
486
- * "Decorator ordering & the microtask trick"`.
537
+ * **Decorator-ordering note.** TypeScript decorators apply bottom-up, so
538
+ * `@ZodResponse` (typically written above `@Get` / `@HttpCode` /
539
+ * `@Header(...)`) runs *before* those siblings have written their
540
+ * `HTTP_CODE_METADATA` / `METHOD_METADATA` / `HEADERS_METADATA`. The
541
+ * `@ApiResponse(...)` call is therefore always deferred via `queueMicrotask`,
542
+ * so by the time it reads sibling metadata — the effective status *and* the
543
+ * effective content type (including `@Header('Content-Type', )` inference) —
544
+ * everything has settled. The document is built much later
545
+ * (`SwaggerModule.createDocument`), so the deferral is invisible to output.
546
+ * See `docs/responses.md → "Decorator ordering & the microtask trick"`.
487
547
  */
488
548
  declare const ZodResponse: (opts: ZodResponseOptions) => MethodDecorator;
489
549
 
@@ -586,6 +646,7 @@ declare class ZodSerializerInterceptor implements NestInterceptor {
586
646
  private readonly reflector;
587
647
  private readonly logOutputFailure;
588
648
  private readonly createSerializationException;
649
+ private readonly streamMatcher;
589
650
  constructor(reflector: Reflector, options?: NormalizedZodNestOptions);
590
651
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
591
652
  private transform;
@@ -714,4 +775,4 @@ declare class ZodNestDocumentError extends ZodNestError {
714
775
  constructor(code: ZodNestDocumentErrorCode, message: string, details?: Record<string, unknown>);
715
776
  }
716
777
 
717
- export { type ApplyZodNestOptions, COMPONENTS_SCHEMAS_PREFIX, type CreateSerializationException, type CreateValidationException, type CreateZodDtoOptions, DEFAULT_MAX_LOGGED_VALUE_BYTES, DEFAULT_REDACT_KEYS, type Io, type LineageEntry, type NormalizedZodNestOptions, type Override, type OverrideContext, type OverrideJSONSchemaArg, type RegisterSchemaOptions, type ResponseStatusInput, type ResponseStatusWildcard, type ResponseVariant, type ResponseVariantKind, SchemaObject, type ToOpenApiOptions, type ToOpenApiResult, ZOD_DTO_SYMBOL, ZOD_NEST_DTO_EXTENSION, ZOD_NEST_ERROR_DUPLICATE_ID, ZOD_NEST_ERROR_EXTENSION, ZOD_NEST_OPTIONS, ZOD_RESPONSES_METADATA_KEY, ZodBody, type ZodBodyOptions, ZodCookies, type ZodCookiesOptions, type ZodDto, type ZodDtoMarker, ZodHeaders, type ZodHeadersOptions, ZodNestDocumentError, type ZodNestDocumentErrorCode, ZodNestError, ZodNestModule, type ZodNestModuleOptions, type ZodNestRegistry, ZodNestUnrepresentableError, ZodQuery, type ZodQueryOptions, ZodResponse, type ZodResponseDescription, type ZodResponseOptions, type ZodResponseType, ZodSerializationException, ZodSerializerInterceptor, ZodValidationException, ZodValidationPipe, type ZodValidationPipeArg, type ZodValidationPipeOptions, applyZodNest, createRegistry, createZodDto, defaultRegistry, defaultStatusFor, extend, getLineage, isZodDto, isZodDtoMarker, makeZodDtoMarker, overrideJSONSchema, registerSchema, resolveEffectiveStatus, toOpenApi };
778
+ export { type ApplyZodNestOptions, COMPONENTS_SCHEMAS_PREFIX, type CreateSerializationException, type CreateValidationException, type CreateZodDtoOptions, DEFAULT_MAX_LOGGED_VALUE_BYTES, DEFAULT_REDACT_KEYS, DEFAULT_STREAM_CONTENT_TYPES, type Io, type LineageEntry, type NormalizedZodNestOptions, type Override, type OverrideContext, type OverrideJSONSchemaArg, type RegisterSchemaOptions, type ResponseStatusInput, type ResponseStatusWildcard, type ResponseVariant, type ResponseVariantKind, SchemaObject, type ToOpenApiOptions, type ToOpenApiResult, ZOD_DTO_SYMBOL, ZOD_NEST_DTO_EXTENSION, ZOD_NEST_ERROR_DUPLICATE_ID, ZOD_NEST_ERROR_EXTENSION, ZOD_NEST_OPTIONS, ZOD_RESPONSES_METADATA_KEY, ZodBody, type ZodBodyOptions, ZodCookies, type ZodCookiesOptions, type ZodDto, type ZodDtoMarker, ZodHeaders, type ZodHeadersOptions, ZodNestDocumentError, type ZodNestDocumentErrorCode, ZodNestError, ZodNestModule, type ZodNestModuleOptions, type ZodNestRegistry, ZodNestUnrepresentableError, ZodQuery, type ZodQueryOptions, ZodResponse, type ZodResponseDescription, type ZodResponseOptions, type ZodResponseType, ZodSerializationException, ZodSerializerInterceptor, ZodValidationException, ZodValidationPipe, type ZodValidationPipeArg, type ZodValidationPipeOptions, applyZodNest, createRegistry, createZodDto, defaultRegistry, defaultStatusFor, extend, getLineage, isZodDto, isZodDtoMarker, makeZodDtoMarker, overrideJSONSchema, registerSchema, resolveEffectiveStatus, toOpenApi };