zod-nest 1.9.0 → 1.11.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
@@ -23,7 +23,7 @@ For the long-form motivation, see [`docs/why-this-exists.md`](docs/why-this-exis
23
23
  - **OpenAPI 3.1 emission** — no post-processing, no spec downgrade, no leftover internal extension keys.
24
24
  - **`createZodDto`** — class wrapper around a Zod schema with introspectable `schema`, `id`, `io`, and a sibling `Output` class when input/output diverge.
25
25
  - **`ZodValidationPipe`** — auto-detects DTO from handler-arg metatype, accepts an explicit DTO or raw Zod schema, customizable exception factory.
26
- - **`@ZodResponse`** — stackable per status code, accepts a single DTO, an array (`[Dto]`), or a tuple (`[A, B, …]`). No internal `@HttpCode` — caller controls status.
26
+ - **`@ZodResponse`** — stackable per status code; each entry is a DTO **or a raw Zod schema** (single, array `[Entry]`, or tuple `[A, B, …]`, mixable). A raw schema is normalised to an output DTO internally, so a `z.discriminatedUnion` / `z.union` / `z.intersection` that can't be wrapped in `createZodDto` (TS2509) drops straight into a response. No internal `@HttpCode` — caller controls status.
27
27
  - **`@ZodBody` / `@ZodQuery` / `@ZodHeaders` / `@ZodCookies`** — method-level decorators that wire OpenAPI docs for schemas whose `z.infer<>` is a union (intersection-of-union, discriminated unions, etc.) — the ones that can't be wrapped in `createZodDto` because TS refuses unions as class bases (TS2509). Schema validation stays via `@Body(new ZodValidationPipe(schema))`; the type at the handler arg stays `z.infer<>`.
28
28
  - **`ZodSerializerInterceptor`** — response validation with a `passthroughOnError` escape hatch for untrusted upstream shapes.
29
29
  - **`applyZodNest`** — one call after `SwaggerModule.createDocument(...)` replaces the entire `cleanupOpenApiDoc` ritual.
@@ -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() {
@@ -154,7 +157,7 @@ See [`docs/dto.md`](docs/dto.md) for the full surface.
154
157
 
155
158
  ### Schemas that don't fit a class
156
159
 
157
- 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:
158
161
 
159
162
  ```ts
160
163
  const IntersectionWithUnion = z
@@ -207,6 +210,18 @@ At request time, `ZodSerializerInterceptor` looks at `response.statusCode` and p
207
210
 
208
211
  See [`docs/responses.md`](docs/responses.md) for the precedence chain and `passthroughOnError`.
209
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
+
210
225
  ## Usage
211
226
 
212
227
  ### Creating DTOs
@@ -228,12 +243,18 @@ The id is what appears as the `components.schemas` key in the OpenAPI document a
228
243
 
229
244
  ```ts
230
245
  // Preferred — on the schema, via Zod's metadata
231
- const userSchema = z.object({ /* ... */ }).meta({ id: 'User' });
246
+ const userSchema = z
247
+ .object({
248
+ /* ... */
249
+ })
250
+ .meta({ id: 'User' });
232
251
  class UserDto extends createZodDto(userSchema) {}
233
252
 
234
253
  // Also valid — passed through createZodDto's options
235
254
  class UserDto extends createZodDto(
236
- z.object({ /* ... */ }),
255
+ z.object({
256
+ /* ... */
257
+ }),
237
258
  { id: 'User' },
238
259
  ) {}
239
260
  ```
@@ -245,8 +266,8 @@ If you pass neither, the class name is used as a fallback. Under minification
245
266
  ### Input validation
246
267
 
247
268
  ```ts
248
- import { ZodValidationPipe } from 'zod-nest';
249
269
  import { APP_PIPE } from '@nestjs/core';
270
+ import { ZodValidationPipe } from 'zod-nest';
250
271
 
251
272
  @Controller('things')
252
273
  class ThingsController {
@@ -310,14 +331,14 @@ Stack `@ZodResponse` per status code:
310
331
  import { Get, HttpStatus } from '@nestjs/common';
311
332
  import { ZodResponse } from 'zod-nest';
312
333
 
313
- class UserDto extends createZodDto(z.object({ id: z.string() }), { id: 'User' }) {}
314
- 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' }) {}
315
336
  class FatalDto extends createZodDto(z.object({ trace: z.string() }), { id: 'Fatal' }) {}
316
337
 
317
338
  class UsersController {
318
339
  @Get(':id')
319
- @ZodResponse({ type: UserDto }) // 200 inferred
320
- @ZodResponse({ status: HttpStatus.NOT_FOUND, type: ErrorDto })
340
+ @ZodResponse({ type: UserDto }) // 200 inferred
341
+ @ZodResponse({ status: HttpStatus.NOT_FOUND, type: ErrorDto })
321
342
  @ZodResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, type: FatalDto })
322
343
  getUser(): void {}
323
344
  }
@@ -372,8 +393,7 @@ import { ZodNestModule } from 'zod-nest';
372
393
  ZodNestModule.forRoot({
373
394
  validationLogs: { input: true, output: true },
374
395
  redactKeys: ['password', 'token', 'sessionId'],
375
- createSerializationException: (err, ctx) =>
376
- new MyCustomFiveHundred(err, ctx),
396
+ createSerializationException: (err, ctx) => new MyCustomFiveHundred(err, ctx),
377
397
  }),
378
398
  ],
379
399
  controllers: [UsersController],
@@ -385,14 +405,14 @@ class AppModule {}
385
405
 
386
406
  ## Module options
387
407
 
388
- | Option | Type | Default | What it does |
389
- |---|---|---|---|
390
- | `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 |
391
411
  | `createSerializationException` | `(err, executionContext) => unknown` | uses `ZodSerializationException` | Custom 500 exception for output failures (strict mode only) |
392
- | `validationLogs` | `boolean \| { input?, output? }` | `false` | Opt-in failure-only logging |
393
- | `logger` | `LoggerService` | NestJS `Logger` | Replace the logger (pino, winston, …) |
394
- | `redactKeys` | `readonly string[]` | `DEFAULT_REDACT_KEYS` | Keys redacted in logs (replaces default list, no merge) |
395
- | `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 |
396
416
 
397
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.
398
418
 
@@ -412,7 +432,7 @@ A log entry carries:
412
432
 
413
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.
414
434
 
415
- 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`:
416
436
 
417
437
  ```ts
418
438
  import { DEFAULT_REDACT_KEYS, ZodNestModule } from 'zod-nest';
@@ -450,27 +470,35 @@ See [`docs/composition.md`](docs/composition.md) for the full contract, current
450
470
  A compact, link-out index. Type signatures and detailed semantics live in the companion docs.
451
471
 
452
472
  **DTO** — [`docs/dto.md`](docs/dto.md)
453
- - `createZodDto(schema, options?)`, `isZodDto(value)`, `ZodDto<TSchema>`, `Io`
473
+
474
+ - `createZodDto(schema, options?)`, `isZodDto(value)`, `isZodSchema(value)`, `ZodDto<TSchema>`, `Io`
454
475
 
455
476
  **Validation** — [`docs/validation-pipe.md`](docs/validation-pipe.md)
477
+
456
478
  - `ZodValidationPipe`, `ZodValidationException`, `ZodValidationPipeOptions`, `CreateValidationException`
457
479
 
458
480
  **Response** — [`docs/responses.md`](docs/responses.md)
459
- - `@ZodResponse({ status?, type, description?, passthroughOnError? })`, `ZodSerializerInterceptor`, `ZodSerializationException`, `defaultStatusFor`, `resolveEffectiveStatus`, `ResponseStatusInput`, `ResponseStatusWildcard`, `ResponseVariant`, `ZOD_RESPONSES_METADATA_KEY`
481
+
482
+ - `@ZodResponse({ status?, type, description?, passthroughOnError?, contentType?, stream? })` (`type` is a DTO or raw schema, or an array/tuple of either), `ZodSerializerInterceptor`, `ZodSerializationException`, `defaultStatusFor`, `resolveEffectiveStatus`, `ResponseStatusInput`, `ResponseStatusWildcard`, `ResponseVariant`, `ZodResponseEntry`, `ZodResponseType`, `ZOD_RESPONSES_METADATA_KEY`
460
483
 
461
484
  **Parameter decorators for raw schemas** — [`docs/recipes/intersection-with-union.md`](docs/recipes/intersection-with-union.md)
485
+
462
486
  - `@ZodBody(schema, options?)`, `@ZodQuery(schema, options?)`, `@ZodHeaders(schema, options?)`, `@ZodCookies(schema, options?)`, `ZodBodyOptions`, `ZodQueryOptions`, `ZodHeadersOptions`, `ZodCookiesOptions`
463
487
 
464
488
  **Document** — [`docs/swagger-integration.md`](docs/swagger-integration.md)
489
+
465
490
  - `applyZodNest(rawDoc, options)`, `ApplyZodNestOptions`, `ZodNestDocumentError`
466
491
 
467
492
  **Module** — [`docs/module-options.md`](docs/module-options.md) / [`docs/logging.md`](docs/logging.md)
493
+
468
494
  - `ZodNestModule.forRoot(options?)`, `ZodNestModuleOptions`, `DEFAULT_REDACT_KEYS`, `DEFAULT_MAX_LOGGED_VALUE_BYTES`, `ZOD_NEST_OPTIONS`
469
495
 
470
496
  **Schema engine** — single-schema mode and extension points
497
+
471
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`
472
499
 
473
500
  **Helpers** (subpath: `zod-nest/helpers`) — common JSON Schema fragments + presets for assembling overrides
501
+
474
502
  - **Fragment catalog** (frozen consts): `dateTimeFragment`, `dateFragment`, `timeFragment`, `uuidFragment`, `emailFragment`, `uriFragment`, `hostnameFragment`, `ipv4Fragment`, `ipv6Fragment`, `binaryFragment`, `byteFragment`, `int32Fragment`, `int64Fragment`, `floatFragment`, `doubleFragment`, `opaqueFragment`
475
503
  - **Sugar functions**: `binary(opts?)`, `opaque(opts?)`
476
504
  - **Type-strict composition**: `enrich(base, extras)` — extras are typed per fragment family
@@ -480,31 +508,31 @@ See [`docs/recipes/custom-openapi-overrides.md`](docs/recipes/custom-openapi-ove
480
508
 
481
509
  ## Documentation
482
510
 
483
- | Topic | Doc |
484
- |---|---|
485
- | Why this library exists | [`docs/why-this-exists.md`](docs/why-this-exists.md) |
486
- | `createZodDto` in depth | [`docs/dto.md`](docs/dto.md) |
487
- | Input validation | [`docs/validation-pipe.md`](docs/validation-pipe.md) |
488
- | Responses, multi-status, status resolution | [`docs/responses.md`](docs/responses.md) |
489
- | Module options reference | [`docs/module-options.md`](docs/module-options.md) |
490
- | Validation logging | [`docs/logging.md`](docs/logging.md) |
491
- | Swagger integration & custom emission | [`docs/swagger-integration.md`](docs/swagger-integration.md) |
492
- | Composition (experimental) | [`docs/composition.md`](docs/composition.md) |
493
- | Exception classes | [`docs/exceptions.md`](docs/exceptions.md) |
494
- | 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/) |
495
523
 
496
524
  ## Compatibility matrix
497
525
 
498
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.
499
527
 
500
- | Cell | `zod` | `@nestjs/common` | `@nestjs/core` | `@nestjs/swagger` | `rxjs` | `reflect-metadata` |
501
- |---|---|---|---|---|---|---|
502
- | `floor` | 4.4.0 | 11.0.1 | 11.0.1 | 11.0.0 | 7.6.0 | 0.2.0 |
503
- | `zod-latest` | latest | 11.0.1 | 11.0.1 | 11.0.0 | 7.6.0 | 0.2.0 |
504
- | `nest-latest` | 4.4.0 | latest | latest | latest | 7.6.0 | 0.2.0 |
505
- | `rxjs-latest` | 4.4.0 | 11.0.1 | 11.0.1 | 11.0.0 | latest | 0.2.0 |
506
- | `reflect-metadata-latest` | 4.4.0 | 11.0.1 | 11.0.1 | 11.0.0 | 7.6.0 | latest |
507
- | `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 |
508
536
 
509
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`.
510
538
 
package/dist/index.d.mts CHANGED
@@ -225,6 +225,16 @@ declare const isZodDtoMarker: (value: unknown) => value is ZodDtoMarker;
225
225
  * NestJS exposes via `ArgumentMetadata`.
226
226
  */
227
227
  declare const isZodDto: (value: unknown) => value is ZodDto;
228
+ /**
229
+ * Runtime guard: is `value` a Zod schema (any `z.*` type)?
230
+ *
231
+ * Used by `@ZodResponse` to accept a raw schema in place of a DTO — including
232
+ * `z.discriminatedUnion` / `z.union` / `z.intersection`, which can't be wrapped
233
+ * with `createZodDto` (their `z.infer` is a union, so `class … extends
234
+ * createZodDto(schema)` fails to typecheck with TS2509). `instanceof z.ZodType`
235
+ * holds for every schema built through the same `zod` module instance.
236
+ */
237
+ declare const isZodSchema: (value: unknown) => value is z.ZodType;
228
238
 
229
239
  /**
230
240
  * Default exception thrown by `ZodSerializerInterceptor` in strict mode
@@ -292,6 +302,93 @@ interface ValidationLogContext {
292
302
  }
293
303
  type LogValidationFailure = (err: z.ZodError, value: unknown, ctx: ValidationLogContext) => void;
294
304
 
305
+ /**
306
+ * Public metadata key for the array of `ResponseVariant` records attached
307
+ * to handler methods by `@ZodResponse(...)`. Symbol.for() so external
308
+ * consumers (e.g. custom interceptors or doc builders) can read the same
309
+ * registry across realms.
310
+ */
311
+ declare const ZOD_RESPONSES_METADATA_KEY: unique symbol;
312
+ type ResponseVariantKind = 'single' | 'array' | 'tuple';
313
+ /**
314
+ * OpenAPI 3.1 range keys accepted by `@ZodResponse({ status })`. Matched
315
+ * by `ZodSerializerInterceptor` after exact numeric matches fail —
316
+ * `'2XX'` covers 200–299, `'4XX'` covers 400–499, and so on.
317
+ */
318
+ type ResponseStatusWildcard = '1XX' | '2XX' | '3XX' | '4XX' | '5XX';
319
+ /**
320
+ * Description payload accepted by `@ZodResponse(...)` and passed through to
321
+ * `applyZodNest`'s `@ApiResponse(...)` emitter. String form is shorthand for
322
+ * `{ description }`; the object form lets users declare OpenAPI response
323
+ * `headers` / `links` alongside the description.
324
+ */
325
+ type ZodResponseDescription = string | {
326
+ description: string;
327
+ headers?: Record<string, unknown>;
328
+ links?: Record<string, unknown>;
329
+ };
330
+ /**
331
+ * One variant record per `@ZodResponse(...)` call. `dto` is kept alongside
332
+ * `validationSchema` so `applyZodNest` can emit `@ApiResponse({ type })`
333
+ * without unwrapping the runtime-only `z.array(...)` / `z.tuple([...])` wrapper.
334
+ *
335
+ * `status` is `undefined` when the user didn't pass one explicitly OR when
336
+ * the user wrote `'default'` (the decorator collapses both to `undefined` —
337
+ * they mean the same thing: "resolve to the method's default status at
338
+ * request time"). The effective status is resolved lazily by
339
+ * `resolveEffectiveStatus(variant, handler)` because `@ZodResponse` runs
340
+ * *before* NestJS' route decorators (`@Get`, `@Post`, ...) under
341
+ * TypeScript's bottom-up application order, so `METHOD_METADATA` is not
342
+ * yet set when the decorator evaluates.
343
+ *
344
+ * A wildcard string (`'2XX'` / `'4XX'` / ...) is kept verbatim — the
345
+ * matcher in `ZodSerializerInterceptor` handles range comparison.
346
+ */
347
+ interface ResponseVariant {
348
+ status: number | ResponseStatusWildcard | undefined;
349
+ kind: ResponseVariantKind;
350
+ dto: ZodDto | readonly ZodDto[];
351
+ validationSchema: z.ZodType;
352
+ description?: ZodResponseDescription;
353
+ passthroughOnError: boolean;
354
+ /**
355
+ * Raw `contentType` option from `@ZodResponse`, if set. `undefined` means
356
+ * "resolve lazily" — `resolveContentType` falls back to a stream-typed
357
+ * `@Header('Content-Type', …)` or `application/json`. Drives the OpenAPI
358
+ * media-type key.
359
+ */
360
+ contentType?: string;
361
+ /**
362
+ * Raw `stream` option from `@ZodResponse`, if set. `undefined` means
363
+ * "infer" — `isStreamResponse` treats the response as a stream when the
364
+ * effective content type is a known stream type. Streams skip validation.
365
+ */
366
+ stream?: boolean;
367
+ }
368
+
369
+ /**
370
+ * Content types whose bodies zod-nest treats as streams by default: the
371
+ * handler writes them straight to the response buffer (SSE, NDJSON, raw
372
+ * binary, files), so `ZodSerializerInterceptor` skips validation and the
373
+ * OpenAPI media-type key becomes the stream type rather than `application/json`.
374
+ *
375
+ * A trailing `/*` marks a family prefix — `image/*` matches `image/png`,
376
+ * `image/jpeg`, … — everything else matches exactly. Consumers extend this set
377
+ * *additively* via `ZodNestModuleOptions.streamContentTypes`; the defaults are
378
+ * always retained so SSE / NDJSON detection can't be accidentally dropped.
379
+ */
380
+ declare const DEFAULT_STREAM_CONTENT_TYPES: readonly string[];
381
+ /**
382
+ * Normalised stream-content-type matcher: exact media types in a `Set` for
383
+ * O(1) lookup, family prefixes (`image/`, …) checked with `startsWith`. Built
384
+ * once by `normalizeStreamMatcher` from the default list (decoration time) or
385
+ * the default ∪ module-configured list (runtime).
386
+ */
387
+ interface StreamContentTypeMatcher {
388
+ exact: ReadonlySet<string>;
389
+ prefixes: readonly string[];
390
+ }
391
+
295
392
  /**
296
393
  * Module-scope factory for the exception thrown by `ZodValidationPipe` on
297
394
  * input validation failure. Mirrors the existing per-pipe option but lives
@@ -329,6 +426,14 @@ interface ZodNestModuleOptions {
329
426
  * values become `{ _truncated: true, _originalBytes, _preview }`.
330
427
  */
331
428
  maxLoggedValueBytes?: number;
429
+ /**
430
+ * Additional response content types treated as streams — written directly
431
+ * to the response buffer and never validated by `ZodSerializerInterceptor`.
432
+ * MERGED with the built-in `DEFAULT_STREAM_CONTENT_TYPES` (SSE, NDJSON,
433
+ * octet-stream, pdf, `image/*`, `audio/*`, `video/*`), so the defaults are
434
+ * always retained. A trailing `/*` entry matches a media-type family.
435
+ */
436
+ streamContentTypes?: readonly string[];
332
437
  }
333
438
  /**
334
439
  * Resolved options consumed by pipe + interceptor. `forRoot()` builds this
@@ -342,6 +447,8 @@ interface NormalizedZodNestOptions {
342
447
  logInputFailure: LogValidationFailure;
343
448
  /** No-op when `validationLogs.output` resolved to false. */
344
449
  logOutputFailure: LogValidationFailure;
450
+ /** Built-in defaults ∪ `streamContentTypes`, used by the interceptor's stream check. */
451
+ streamMatcher: StreamContentTypeMatcher;
345
452
  }
346
453
  declare const DEFAULT_REDACT_KEYS: readonly string[];
347
454
  declare const DEFAULT_MAX_LOGGED_VALUE_BYTES = 4096;
@@ -380,69 +487,28 @@ declare class ZodValidationPipe implements PipeTransform {
380
487
  }
381
488
 
382
489
  /**
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.
490
+ * One entry in `@ZodResponse({ type })` either a zod-nest DTO or a raw Zod
491
+ * schema. A raw schema is normalised internally to `createZodDto(schema).Output`
492
+ * (output IO, since a response body is output-only), so schemas that can't be
493
+ * wrapped with `createZodDto` — `z.discriminatedUnion` / `z.union` /
494
+ * `z.intersection`, whose `z.infer` is a union and so trip TS2509 in an
495
+ * `extends` clause — can be passed straight to a response. See `toResponseDto`.
423
496
  */
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
-
497
+ type ZodResponseEntry = ZodDto | z.ZodType;
433
498
  /**
434
- * Accepted shapes for `@ZodResponse({ type })`:
435
- * - `Dto` validates as `Dto.schema` (single-DTO response).
436
- * - `[Dto]` (length 1) → validates as `z.array(Dto.schema)`; matches Nest's
499
+ * Accepted shapes for `@ZodResponse({ type })`. Each slot is a `ZodResponseEntry`
500
+ * (DTO or raw schema), and the two may be mixed within an array:
501
+ * - `Dto` / `Schema` → validates as the entry's schema (single response).
502
+ * - `[Entry]` (length 1) → validates as `z.array(entry.schema)`; matches Nest's
437
503
  * `@ApiResponse({ isArray: true })` convention without minting a separate
438
504
  * `*sDto` id.
439
505
  * - `[A, B, ...]` (length ≥ 2) → validates as `z.tuple([A.schema, B.schema, ...])`;
440
506
  * surfaces as an OpenAPI 3.1 `prefixItems` tuple via `applyZodNest`.
441
507
  *
442
- * Empty arrays and non-DTO elements throw `TypeError` at decoration time
443
- * so typos surface at module load, not the first request.
508
+ * Empty arrays and entries that are neither a DTO nor a schema throw `TypeError`
509
+ * at decoration time so typos surface at module load, not the first request.
444
510
  */
445
- type ZodResponseType = ZodDto | readonly [ZodDto, ...ZodDto[]];
511
+ type ZodResponseType = ZodResponseEntry | readonly [ZodResponseEntry, ...ZodResponseEntry[]];
446
512
  /**
447
513
  * Accepted shapes for `@ZodResponse({ status })`:
448
514
  * - `number` — exact match against `response.statusCode` (most common case).
@@ -462,6 +528,20 @@ interface ZodResponseOptions {
462
528
  type: ZodResponseType;
463
529
  description?: ZodResponseDescription;
464
530
  passthroughOnError?: boolean;
531
+ /**
532
+ * Response content type, used as the OpenAPI media-type key (`@ApiResponse`
533
+ * `content`). Defaults to `'application/json'`. Set a stream type
534
+ * (`'text/event-stream'`, `'application/x-ndjson'`, …) to document a streamed
535
+ * body; combined with `stream` it also turns off response validation.
536
+ */
537
+ contentType?: string;
538
+ /**
539
+ * When `true`, the response is written directly to the buffer and
540
+ * `ZodSerializerInterceptor` skips validation. When unset, it is inferred:
541
+ * a stream-typed `contentType` (or a stream-typed `@Header('Content-Type', …)`)
542
+ * implies `true`. See `DEFAULT_STREAM_CONTENT_TYPES`.
543
+ */
544
+ stream?: boolean;
465
545
  }
466
546
  /**
467
547
  * Method-only decorator. Declares a typed response variant for the handler
@@ -474,16 +554,16 @@ interface ZodResponseOptions {
474
554
  * `'NXX'` wildcard). The wrapped Zod schema (array / tuple) is built once
475
555
  * at decoration time — no per-request schema construction.
476
556
  *
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"`.
557
+ * **Decorator-ordering note.** TypeScript decorators apply bottom-up, so
558
+ * `@ZodResponse` (typically written above `@Get` / `@HttpCode` /
559
+ * `@Header(...)`) runs *before* those siblings have written their
560
+ * `HTTP_CODE_METADATA` / `METHOD_METADATA` / `HEADERS_METADATA`. The
561
+ * `@ApiResponse(...)` call is therefore always deferred via `queueMicrotask`,
562
+ * so by the time it reads sibling metadata — the effective status *and* the
563
+ * effective content type (including `@Header('Content-Type', )` inference) —
564
+ * everything has settled. The document is built much later
565
+ * (`SwaggerModule.createDocument`), so the deferral is invisible to output.
566
+ * See `docs/responses.md → "Decorator ordering & the microtask trick"`.
487
567
  */
488
568
  declare const ZodResponse: (opts: ZodResponseOptions) => MethodDecorator;
489
569
 
@@ -586,6 +666,7 @@ declare class ZodSerializerInterceptor implements NestInterceptor {
586
666
  private readonly reflector;
587
667
  private readonly logOutputFailure;
588
668
  private readonly createSerializationException;
669
+ private readonly streamMatcher;
589
670
  constructor(reflector: Reflector, options?: NormalizedZodNestOptions);
590
671
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
591
672
  private transform;
@@ -714,4 +795,4 @@ declare class ZodNestDocumentError extends ZodNestError {
714
795
  constructor(code: ZodNestDocumentErrorCode, message: string, details?: Record<string, unknown>);
715
796
  }
716
797
 
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 };
798
+ 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 ZodResponseEntry, type ZodResponseOptions, type ZodResponseType, ZodSerializationException, ZodSerializerInterceptor, ZodValidationException, ZodValidationPipe, type ZodValidationPipeArg, type ZodValidationPipeOptions, applyZodNest, createRegistry, createZodDto, defaultRegistry, defaultStatusFor, extend, getLineage, isZodDto, isZodDtoMarker, isZodSchema, makeZodDtoMarker, overrideJSONSchema, registerSchema, resolveEffectiveStatus, toOpenApi };