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 +69 -41
- package/dist/index.d.mts +147 -66
- package/dist/index.d.ts +147 -66
- package/dist/index.js +223 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +223 -41
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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:
|
|
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
|
|
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
|
|
314
|
-
class ErrorDto extends createZodDto(z.object({ code: z.number() }),
|
|
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({
|
|
320
|
-
@ZodResponse({ status: HttpStatus.NOT_FOUND,
|
|
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
|
|
389
|
-
|
|
390
|
-
| `createValidationException`
|
|
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`
|
|
393
|
-
| `logger`
|
|
394
|
-
| `redactKeys`
|
|
395
|
-
| `maxLoggedValueBytes`
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
484
|
-
|
|
485
|
-
| Why this library exists
|
|
486
|
-
| `createZodDto` in depth
|
|
487
|
-
| Input validation
|
|
488
|
-
| Responses, multi-status, status resolution | [`docs/responses.md`](docs/responses.md)
|
|
489
|
-
| Module options reference
|
|
490
|
-
| Validation logging
|
|
491
|
-
| Swagger integration & custom emission
|
|
492
|
-
| Composition (experimental)
|
|
493
|
-
| Exception classes
|
|
494
|
-
| 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
|
|
501
|
-
|
|
502
|
-
| `floor`
|
|
503
|
-
| `zod-latest`
|
|
504
|
-
| `nest-latest`
|
|
505
|
-
| `rxjs-latest`
|
|
506
|
-
| `reflect-metadata-latest` | 4.4.0
|
|
507
|
-
| `all-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
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
436
|
-
* - `
|
|
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
|
|
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 =
|
|
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
|
|
478
|
-
*
|
|
479
|
-
* `@
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
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 };
|