zod-nest 0.6.0 → 0.8.1

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 ADDED
@@ -0,0 +1,474 @@
1
+ # zod-nest
2
+
3
+ > Modern **Zod v4** ↔ **NestJS** ↔ **OpenAPI 3.1** integration.
4
+
5
+ [![npm](https://img.shields.io/npm/v/zod-nest)](https://www.npmjs.com/package/zod-nest)
6
+ [![CI](https://github.com/rodrigowbazevedo/zod-nest/actions/workflows/ci.yml/badge.svg)](https://github.com/rodrigowbazevedo/zod-nest/actions/workflows/ci.yml)
7
+ [![codecov](https://codecov.io/gh/rodrigowbazevedo/zod-nest/branch/main/graph/badge.svg)](https://codecov.io/gh/rodrigowbazevedo/zod-nest)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ Define your DTOs once with Zod, get validated request bodies, validated response bodies, and a correct OpenAPI 3.1 document — without the dual-codepath, post-process, or `@ts-ignore` baggage that comes with bolting Zod onto class-validator-shaped tooling.
11
+
12
+ ## Why this exists
13
+
14
+ `zod-nest` is a fresh take on the idea pioneered by [`nestjs-zod`](https://github.com/BenLorantfy/nestjs-zod) — many thanks to that project and its maintainers; this library would not exist without it.
15
+
16
+ The difference is that `zod-nest` is Zod v4 only and OpenAPI 3.1 only. It drops `class-validator` / `class-transformer` coexistence, drops Zod v3 codepaths, drops `cleanupOpenApiDoc` as a separate post-process, and drops the 20-odd `@ts-ignore`s that the dual-version approach required. The result is a smaller surface, fully type-safe end to end, with extension points where you actually need them — exception factories, response status resolution, custom emission overrides.
17
+
18
+ For the long-form motivation, see [`docs/why-this-exists.md`](docs/why-this-exists.md).
19
+
20
+ ## Features
21
+
22
+ - **Zod v4 only** — no v3 dual codepath, no compatibility shim.
23
+ - **OpenAPI 3.1 emission** — no post-processing, no spec downgrade, no leftover internal extension keys.
24
+ - **`createZodDto`** — class wrapper around a Zod schema with introspectable `schema`, `id`, `io`, and a sibling `Output` class when input/output diverge.
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.
27
+ - **`ZodSerializerInterceptor`** — response validation with a `passthroughOnError` escape hatch for untrusted upstream shapes.
28
+ - **`applyZodNest`** — one call after `SwaggerModule.createDocument(...)` replaces the entire `cleanupOpenApiDoc` ritual.
29
+ - **`ZodNestModule.forRoot`** — global pipe + interceptor + logging configuration in one place; optional (everything works standalone).
30
+ - **Validation logging** — opt-in, per-side (input/output), with case-insensitive deep-key redaction and oversize-value truncation.
31
+ - **Composition** — `extend` + `getLineage` emit OpenAPI `allOf` for derived schemas (`@experimental` — see [Composition](#composition-experimental)).
32
+ - **Custom registry support** — `createRegistry()` for explicit isolation, `defaultRegistry` for the common process-wide case.
33
+ - **Doc-build error reporting** — `ZodNestDocumentError` with codes `AMBIGUOUS_RENAME` and `DANGLING_REF` so registry mis-configurations fail in CI, not at runtime.
34
+ - **Strict-mode unrepresentable detection** — `ZodNestUnrepresentableError` surfaces bigint/date/transform constructs that JSON Schema can't represent (opt-out via `strict: false`).
35
+ - **Custom emission overrides** — `Override` callback for file uploads, opaque blobs, or anything else Zod doesn't model.
36
+
37
+ ## Differences from `nestjs-zod`
38
+
39
+ A short list of behavioural differences you'll hit on day one. Full migration table is in [`MIGRATION.md`](MIGRATION.md).
40
+
41
+ - **Multi-status `@ZodResponse`** — stack the decorator per status code. In `nestjs-zod`, multi-status required mixing `@ZodSerializerDto` with hand-rolled `@ApiResponse({ status: ... })` calls.
42
+ - **No internal `@HttpCode`** — `@ZodResponse` does **not** call `@HttpCode` under the hood. Status resolution precedence: `@ZodResponse({ status })` → `@HttpCode(...)` on the handler → method default (`POST → 201`, others → `200`). The caller controls `201` vs `200` vs `204` via standard NestJS decorators.
43
+ - **I/O suffix only when needed** — `<Id>Output` is only emitted when the input and output JSON Schemas actually differ. `nestjs-zod` always emitted `_Output`.
44
+ - **OpenAPI 3.1 only** — no `3.0` fallback. `$ref`s emit to the final location; `cleanupOpenApiDoc` is unnecessary.
45
+ - **Validation-failure logging out of the box** — `nestjs-zod` has none.
46
+ - **Customizable serialization exception** — both `ZodValidationPipe` and `ZodSerializerInterceptor` accept a factory. `nestjs-zod` only customized the input side.
47
+ - **DTO discriminator** — `Symbol.for('zod-nest.dto')` (cross-realm safe), not `MyDto.isZodDto`.
48
+ - **Codec mode in the schema** — express transforms via `z.pipe` / `z.transform`. No `{ codec: true }` flag.
49
+ - **Markers stripped** — the final document has zero `x-zod-nest-*` extensions. `nestjs-zod` left ~10 `x-nestjs_zod-*` keys behind.
50
+
51
+ ## Non-goals (v0)
52
+
53
+ - **Zod v3 support** — Zod v4 only. Migrate first.
54
+ - **`class-validator` / `class-transformer` coexistence** — `zod-nest` is Zod-native. Mixing class-validator decorators on a `createZodDto` result is not supported.
55
+ - **Status wildcards** in `@ZodResponse` (`'2XX'`, `'default'`) — deferred to v1.
56
+ - **Hybrid DTO projects** — mixing `createZodDto` DTOs with plain `@ApiProperty` classes in the same controller is not tested.
57
+ - **Non-HTTP contexts** — WebSocket gateways, GraphQL resolvers, microservice handlers are out of scope.
58
+
59
+ ## Quickstart
60
+
61
+ ```bash
62
+ npm i zod-nest zod @nestjs/swagger
63
+ ```
64
+
65
+ ```ts
66
+ // user.dto.ts
67
+ import { z } from 'zod';
68
+ import { createZodDto } from 'zod-nest';
69
+
70
+ const userSchema = z
71
+ .object({
72
+ id: z.string(),
73
+ email: z.email().transform((v) => v.toLowerCase()),
74
+ })
75
+ .meta({ id: 'User' });
76
+
77
+ export class UserDto extends createZodDto(userSchema) {}
78
+ ```
79
+
80
+ ```ts
81
+ // users.controller.ts
82
+ import { Body, Controller, Get, Post } from '@nestjs/common';
83
+ import { ZodResponse } from 'zod-nest';
84
+ import { UserDto } from './user.dto';
85
+
86
+ @Controller('users')
87
+ export class UsersController {
88
+ @Get('single')
89
+ @ZodResponse({ type: UserDto })
90
+ single(): UserDto {
91
+ return { id: 'u1', email: 'A@B.COM' } as UserDto; // transform lowercases on the way out
92
+ }
93
+
94
+ @Post()
95
+ @ZodResponse({ type: UserDto })
96
+ create(@Body() body: UserDto): UserDto {
97
+ // body is already validated + parsed by ZodValidationPipe
98
+ return body;
99
+ }
100
+ }
101
+ ```
102
+
103
+ ```ts
104
+ // app.module.ts
105
+ import { Module } from '@nestjs/common';
106
+ import { ZodNestModule } from 'zod-nest';
107
+ import { UsersController } from './users.controller';
108
+
109
+ @Module({
110
+ imports: [ZodNestModule.forRoot({ validationLogs: true })],
111
+ controllers: [UsersController],
112
+ })
113
+ export class AppModule {}
114
+ ```
115
+
116
+ ```ts
117
+ // main.ts
118
+ import { NestFactory } from '@nestjs/core';
119
+ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
120
+ import { applyZodNest } from 'zod-nest';
121
+ import { AppModule } from './app.module';
122
+
123
+ async function bootstrap() {
124
+ const app = await NestFactory.create(AppModule);
125
+
126
+ const raw = SwaggerModule.createDocument(
127
+ app,
128
+ new DocumentBuilder().setTitle('Users').setVersion('1').build(),
129
+ );
130
+ const doc = applyZodNest(raw, { app });
131
+ SwaggerModule.setup('docs', app, doc);
132
+
133
+ await app.listen(3000);
134
+ }
135
+
136
+ bootstrap();
137
+ ```
138
+
139
+ That's the whole loop. Validation, response serialization, and a correct OpenAPI 3.1 document — all driven from one Zod schema per DTO.
140
+
141
+ ## Core concepts
142
+
143
+ ### Schema is the source of truth
144
+
145
+ Every DTO is one Zod schema wrapped in a class. The class exists so NestJS' introspection (parameter metatype, `@nestjs/swagger`) can find it; the validation and the OpenAPI emission both come from the schema directly. You don't repeat the shape with decorators.
146
+
147
+ ### `createZodDto` is a thin bridge
148
+
149
+ 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
+
151
+ See [`docs/dto.md`](docs/dto.md) for the full surface.
152
+
153
+ ### I/O suffix rules
154
+
155
+ If a schema's input and output JSON Schemas are byte-equal (the common case), the OpenAPI doc emits a single `components.schemas[Id]`. If they differ (e.g. a `transform`, a `pipe`, an `.optional().default(x)` field), the doc emits two: `Id` for input, `IdOutput` for output. Response refs are rewritten to `IdOutput` automatically.
156
+
157
+ You don't pick the behaviour with a flag — `applyZodNest` compares the emitted bodies and decides per DTO.
158
+
159
+ ### Multi-status responses + status resolution
160
+
161
+ Stack `@ZodResponse` to declare multiple status codes on one handler:
162
+
163
+ ```ts
164
+ @Get(':id')
165
+ @ZodResponse({ type: UserDto }) // success variant — status inferred (200 for GET)
166
+ @ZodResponse({ status: 404, type: ErrorDto })
167
+ @ZodResponse({ status: 500, type: FatalDto })
168
+ getUser(): void {}
169
+ ```
170
+
171
+ **Recommended style:** omit `status` for the success variant and let it infer from the route, then set `status` explicitly only for the off-happy-path variants. Keeps the signal-to-noise high — the explicit numbers in the snippet above are the ones the reader actually needs to scan for.
172
+
173
+ At request time, `ZodSerializerInterceptor` looks at `response.statusCode` and picks the matching variant. If you don't pass `status`, the variant matches on the handler's default (computed once at request time, in this order: `@HttpCode(n)` → `POST → 201`, everything else → `200`). `@ZodResponse` does **not** internally apply `@HttpCode` — you stay in charge of the actual HTTP status.
174
+
175
+ See [`docs/responses.md`](docs/responses.md) for the precedence chain and `passthroughOnError`.
176
+
177
+ ## Usage
178
+
179
+ ### Creating DTOs
180
+
181
+ ```ts
182
+ import { z } from 'zod';
183
+ import { createZodDto } from 'zod-nest';
184
+
185
+ const userSchema = z.object({ id: z.uuid(), name: z.string() }).meta({ id: 'User' });
186
+ class UserDto extends createZodDto(userSchema) {}
187
+
188
+ UserDto.parse({ id: '00000000-0000-0000-0000-000000000000', name: 'Ada' });
189
+ // → { id: '...', name: 'Ada' }
190
+ ```
191
+
192
+ ### Setting the OpenAPI schema id
193
+
194
+ The id is what appears as the `components.schemas` key in the OpenAPI document and what `$ref`s point at. You can set it in two equivalent ways:
195
+
196
+ ```ts
197
+ // Preferred — on the schema, via Zod's metadata
198
+ const userSchema = z.object({ /* ... */ }).meta({ id: 'User' });
199
+ class UserDto extends createZodDto(userSchema) {}
200
+
201
+ // Also valid — passed through createZodDto's options
202
+ class UserDto extends createZodDto(
203
+ z.object({ /* ... */ }),
204
+ { id: 'User' },
205
+ ) {}
206
+ ```
207
+
208
+ Both produce the same OpenAPI output. `.meta({ id })` is preferred when the schema is hoisted into its own `const`, because the id stays with the schema — composition (`extend(parent, ...)`), shared input/output via `.meta({ id })` on the same schema reference, and any non-DTO use of the schema all pick up the same id without an extra hop through `createZodDto`'s options. Use the `createZodDto(schema, { id })` form when you don't own the schema (e.g. it comes from a third-party module) or when defining a small DTO with an inline schema, where chaining `.meta()` on the inline expression hurts readability.
209
+
210
+ If you pass neither, the class name is used as a fallback. Under minification — where class names become single mangled characters — `zod-nest` falls back to an `_AnonZodDto_N` id and prints a one-time console warning. Set an explicit id either way for production builds.
211
+
212
+ ### Input validation
213
+
214
+ ```ts
215
+ import { ZodValidationPipe } from 'zod-nest';
216
+ import { APP_PIPE } from '@nestjs/core';
217
+
218
+ @Controller('things')
219
+ class ThingsController {
220
+ @Post()
221
+ create(@Body() body: CreateThingDto) {
222
+ return { received: body };
223
+ }
224
+ }
225
+
226
+ @Module({
227
+ controllers: [ThingsController],
228
+ providers: [{ provide: APP_PIPE, useClass: ZodValidationPipe }],
229
+ })
230
+ class AppModule {}
231
+ ```
232
+
233
+ Or use `ZodNestModule.forRoot()` — see [Module options](#module-options) below — to wire the pipe globally along with the response interceptor.
234
+
235
+ On failure the pipe throws `ZodValidationException` (HTTP 400, body `{ statusCode, message: 'Validation failed', errors: z.treeifyError(zodError) }`).
236
+
237
+ ### Custom validation exception
238
+
239
+ ```ts
240
+ import { HttpException, HttpStatus } from '@nestjs/common';
241
+ import { ZodValidationPipe } from 'zod-nest';
242
+
243
+ class UnprocessableEntityException extends HttpException {
244
+ constructor(issuesCount: number) {
245
+ super({ message: 'invalid input', issuesCount }, HttpStatus.UNPROCESSABLE_ENTITY);
246
+ }
247
+ }
248
+
249
+ const pipe = new ZodValidationPipe({
250
+ schema: CreateThingDto,
251
+ createValidationException: (zodError) => new UnprocessableEntityException(zodError.issues.length),
252
+ });
253
+ ```
254
+
255
+ The factory receives `(zodError, argMetadata)` and returns anything `throw`-able. Wire it module-wide via `ZodNestModule.forRoot({ createValidationException: ... })`.
256
+
257
+ ### Single-status response
258
+
259
+ ```ts
260
+ @Controller('users')
261
+ class UsersController {
262
+ @Get('single')
263
+ @ZodResponse({ type: UserDto })
264
+ single(): UserDto {
265
+ return { id: 'u1', email: 'A@B.COM' };
266
+ }
267
+ }
268
+ ```
269
+
270
+ The interceptor validates the return value against `UserDto.schema` and applies any `transform` / `pipe` Zod stages — in the example above, `email` is lowercased to `a@b.com` before the response leaves.
271
+
272
+ ### Multi-status responses
273
+
274
+ Stack `@ZodResponse` per status code:
275
+
276
+ ```ts
277
+ import { Get, HttpStatus } from '@nestjs/common';
278
+ import { ZodResponse } from 'zod-nest';
279
+
280
+ class UserDto extends createZodDto(z.object({ id: z.string() }), { id: 'User' }) {}
281
+ class ErrorDto extends createZodDto(z.object({ code: z.number() }), { id: 'Error' }) {}
282
+ class FatalDto extends createZodDto(z.object({ trace: z.string() }), { id: 'Fatal' }) {}
283
+
284
+ class UsersController {
285
+ @Get(':id')
286
+ @ZodResponse({ type: UserDto }) // 200 inferred
287
+ @ZodResponse({ status: HttpStatus.NOT_FOUND, type: ErrorDto })
288
+ @ZodResponse({ status: HttpStatus.INTERNAL_SERVER_ERROR, type: FatalDto })
289
+ getUser(): void {}
290
+ }
291
+ ```
292
+
293
+ At runtime the interceptor matches `response.statusCode` against each variant's `status`. The OpenAPI doc emits three `responses[200|404|500]` entries with the right DTO ref under each. See [`docs/responses.md`](docs/responses.md) for the full mechanism.
294
+
295
+ ### `passthroughOnError`
296
+
297
+ For an upstream shape you don't fully trust, set `passthroughOnError: true` on the variant. Validation failures are logged (if logging is on) but the original value passes through untouched:
298
+
299
+ ```ts
300
+ @Get('proxied')
301
+ @ZodResponse({ type: ProxyDto, passthroughOnError: true })
302
+ proxied(): unknown {
303
+ return { upstream: 'value', extra: ['raw', 'shape'] };
304
+ }
305
+ ```
306
+
307
+ Use this sparingly — it bypasses the contract you declared. Logging at `warn` severity makes the deviation visible without breaking the request.
308
+
309
+ ### Array and tuple responses
310
+
311
+ ```ts
312
+ @Get('list') @ZodResponse({ type: [UserDto] }) list(): UserDto[] { /* ... */ }
313
+ @Get('pair') @ZodResponse({ type: [UserDto, TagDto] }) pair(): unknown { /* ... */ }
314
+ ```
315
+
316
+ `[Dto]` validates as `z.array(Dto.schema)` and surfaces as OpenAPI `type: array`. `[A, B, …]` validates as `z.tuple([A.schema, B.schema, …])` and surfaces as `prefixItems`. Empty arrays and non-DTO elements throw `TypeError` at decoration time, so typos surface at module load — not the first request.
317
+
318
+ ### Swagger integration
319
+
320
+ ```ts
321
+ import { applyZodNest } from 'zod-nest';
322
+
323
+ const raw = SwaggerModule.createDocument(app, config);
324
+ const doc = applyZodNest(raw, { app });
325
+ SwaggerModule.setup('docs', app, doc);
326
+ ```
327
+
328
+ `applyZodNest` walks the doc, replaces every `x-zod-nest-dto` marker with the real Zod-derived JSON Schema, applies the I/O suffix truth table, strips the markers, and validates the final ref graph. Any dangling ref throws `ZodNestDocumentError({ code: 'DANGLING_REF' })` — the spec fails at boot, not at request time.
329
+
330
+ See [`docs/swagger-integration.md`](docs/swagger-integration.md) for `Override`, custom registries, and strict-mode behaviour.
331
+
332
+ ### Module setup
333
+
334
+ ```ts
335
+ import { ZodNestModule } from 'zod-nest';
336
+
337
+ @Module({
338
+ imports: [
339
+ ZodNestModule.forRoot({
340
+ validationLogs: { input: true, output: true },
341
+ redactKeys: ['password', 'token', 'sessionId'],
342
+ createSerializationException: (err, ctx) =>
343
+ new MyCustomFiveHundred(err, ctx),
344
+ }),
345
+ ],
346
+ controllers: [UsersController],
347
+ })
348
+ class AppModule {}
349
+ ```
350
+
351
+ `forRoot()` is optional — `ZodValidationPipe` and `ZodSerializerInterceptor` work standalone with safe defaults. Use `forRoot` when you want consistent logging, a custom logger, custom exceptions, or shared redaction across the pipe and the interceptor.
352
+
353
+ ## Module options
354
+
355
+ | Option | Type | Default | What it does |
356
+ |---|---|---|---|
357
+ | `createValidationException` | `(err, argMetadata) => unknown` | uses `ZodValidationException` | Custom 400 exception for input failures |
358
+ | `createSerializationException` | `(err, executionContext) => unknown` | uses `ZodSerializationException` | Custom 500 exception for output failures (strict mode only) |
359
+ | `validationLogs` | `boolean \| { input?, output? }` | `false` | Opt-in failure-only logging |
360
+ | `logger` | `LoggerService` | NestJS `Logger` | Replace the logger (pino, winston, …) |
361
+ | `redactKeys` | `readonly string[]` | `DEFAULT_REDACT_KEYS` | Keys redacted in logs (replaces default list, no merge) |
362
+ | `maxLoggedValueBytes` | `number` | `4096` | Truncate oversize logged values |
363
+
364
+ `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.
365
+
366
+ Full reference (with every interaction note) lives in [`docs/module-options.md`](docs/module-options.md).
367
+
368
+ ## Logging
369
+
370
+ Validation logging fires **only on failure**, with side `'input'` or `'output'`. The default behaviour is off; opt in via `validationLogs: true` (both sides) or `validationLogs: { input: true }` / `{ output: true }` (granular).
371
+
372
+ A log entry carries:
373
+
374
+ - `side` — `'input'` or `'output'`
375
+ - `severity` — `'error'` for strict failures, `'warn'` for `passthroughOnError` failures
376
+ - `dto` — the DTO class name
377
+ - `value` — the offending value, redacted and truncated
378
+ - the treeified Zod error from `z.treeifyError(zodError)`
379
+
380
+ 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.
381
+
382
+ Supplying `redactKeys` **replaces** the default list — there is no merge. If you want to *add* keys, spread `DEFAULT_REDACT_KEYS`:
383
+
384
+ ```ts
385
+ import { DEFAULT_REDACT_KEYS, ZodNestModule } from 'zod-nest';
386
+
387
+ ZodNestModule.forRoot({
388
+ validationLogs: true,
389
+ redactKeys: [...DEFAULT_REDACT_KEYS, 'sessionId'],
390
+ });
391
+ ```
392
+
393
+ See [`docs/logging.md`](docs/logging.md) for custom-logger adapters (pino, winston), structured-logging shape, and performance characteristics.
394
+
395
+ ## Composition (experimental)
396
+
397
+ > **`@experimental`** — output shape may change as edge cases surface. Pin a minor version if you build on this surface.
398
+
399
+ `zod-nest` ships an `extend` helper that records a parent → child link and emits OpenAPI `allOf` for derived schemas:
400
+
401
+ ```ts
402
+ import { extend, getLineage } from 'zod-nest';
403
+
404
+ const Base = z.object({ id: z.string() }).meta({ id: 'Base' });
405
+ const Child = extend(Base, (s) => s.extend({ role: z.string() }).meta({ id: 'Child' }));
406
+
407
+ getLineage(Child);
408
+ // → { op: 'extend', parent: Base }
409
+ ```
410
+
411
+ The emitted `Child` schema is `allOf: [{ $ref: '#/components/schemas/Base' }, { type: 'object', properties: { role: { type: 'string' } }, required: ['role'] }]`. The parent must be registered (via `.meta({ id })` or `createZodDto`) for the `$ref` to resolve; anonymous parents fall back to flat emission.
412
+
413
+ See [`docs/composition.md`](docs/composition.md) for the full contract, current limitations, and the roadmap for non-`extend` operators.
414
+
415
+ ## API reference
416
+
417
+ A compact, link-out index. Type signatures and detailed semantics live in the companion docs.
418
+
419
+ **DTO** — [`docs/dto.md`](docs/dto.md)
420
+ - `createZodDto(schema, options?)`, `isZodDto(value)`, `ZodDto<TSchema>`, `Io`
421
+
422
+ **Validation** — [`docs/validation-pipe.md`](docs/validation-pipe.md)
423
+ - `ZodValidationPipe`, `ZodValidationException`, `ZodValidationPipeOptions`, `CreateValidationException`
424
+
425
+ **Response** — [`docs/responses.md`](docs/responses.md)
426
+ - `@ZodResponse({ status?, type, description?, passthroughOnError? })`, `ZodSerializerInterceptor`, `ZodSerializationException`, `defaultStatusFor`, `resolveEffectiveStatus`, `ResponseVariant`, `ZOD_RESPONSES_METADATA_KEY`
427
+
428
+ **Document** — [`docs/swagger-integration.md`](docs/swagger-integration.md)
429
+ - `applyZodNest(rawDoc, options)`, `ApplyZodNestOptions`, `ZodNestDocumentError`
430
+
431
+ **Module** — [`docs/module-options.md`](docs/module-options.md) / [`docs/logging.md`](docs/logging.md)
432
+ - `ZodNestModule.forRoot(options?)`, `ZodNestModuleOptions`, `DEFAULT_REDACT_KEYS`, `DEFAULT_MAX_LOGGED_VALUE_BYTES`, `ZOD_NEST_OPTIONS`
433
+
434
+ **Schema engine** — single-schema mode and extension points
435
+ - `toOpenApi(schema, opts)`, `createRegistry()`, `defaultRegistry`, `ZodNestRegistry`, `Override`, `OverrideContext`, `ZodNestError`, `ZodNestUnrepresentableError`, `extend`, `getLineage`, `LineageEntry`
436
+
437
+ ## Documentation
438
+
439
+ | Topic | Doc |
440
+ |---|---|
441
+ | Why this library exists | [`docs/why-this-exists.md`](docs/why-this-exists.md) |
442
+ | `createZodDto` in depth | [`docs/dto.md`](docs/dto.md) |
443
+ | Input validation | [`docs/validation-pipe.md`](docs/validation-pipe.md) |
444
+ | Responses, multi-status, status resolution | [`docs/responses.md`](docs/responses.md) |
445
+ | Module options reference | [`docs/module-options.md`](docs/module-options.md) |
446
+ | Validation logging | [`docs/logging.md`](docs/logging.md) |
447
+ | Swagger integration & custom emission | [`docs/swagger-integration.md`](docs/swagger-integration.md) |
448
+ | Composition (experimental) | [`docs/composition.md`](docs/composition.md) |
449
+ | Exception classes | [`docs/exceptions.md`](docs/exceptions.md) |
450
+ | Recipes | [`docs/recipes/`](docs/recipes/) |
451
+
452
+ ## Migration from `nestjs-zod`
453
+
454
+ If you're coming from `nestjs-zod`, the headline changes are:
455
+
456
+ - Replace `cleanupOpenApiDoc(SwaggerModule.createDocument(app, config))` with `applyZodNest(SwaggerModule.createDocument(app, config), { app })`.
457
+ - Replace `@ApiOkResponse({ type: Dto }) + @ZodSerializerDto(Dto)` pairs with `@ZodResponse({ type: Dto })`.
458
+ - Drop `class-validator` / `class-transformer` if they were installed only for `nestjs-zod` interop.
459
+ - Check any `MyDto.isZodDto` reflection — the discriminator is now `Symbol.for('zod-nest.dto') in MyDto`.
460
+ - Status wildcards (`'2XX'`, `'default'`) aren't supported in v0 — use explicit statuses.
461
+
462
+ Full guide with side-by-side diffs and a 19-row breaking-changes table in [`MIGRATION.md`](MIGRATION.md).
463
+
464
+ ## Contributing
465
+
466
+ `zod-nest` is a young, single-maintainer OSS project — contributions and issues are welcome. The codebase is well-tested (>340 tests, full coverage on document/schema layers) and is meant to stay small enough that a first-time contributor can hold the whole surface in their head.
467
+
468
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for local-dev setup, the test layout, and how to add a recipe. Reports and discussions go through [GitHub issues](https://github.com/rodrigowbazevedo/zod-nest/issues).
469
+
470
+ ## License
471
+
472
+ MIT — see [`LICENSE`](LICENSE).
473
+
474
+ The names and patterns `createZodDto`, `ZodValidationPipe`, `ZodValidationException`, `ZodSerializerInterceptor`, `ZodSerializationException`, and `ZodResponse` originate in [`nestjs-zod`](https://github.com/BenLorantfy/nestjs-zod) (MIT). Attribution lives in [`NOTICE`](NOTICE).
package/dist/index.d.mts CHANGED
@@ -12,10 +12,10 @@ declare const ZOD_NEST_ERROR_EXTENSION = "x-zod-nest-error";
12
12
  /** Value of `x-zod-nest-error` when a registry id is claimed by more than one schema. */
13
13
  declare const ZOD_NEST_ERROR_DUPLICATE_ID = "duplicate-id";
14
14
  /**
15
- * OpenAPI extension key used by `createZodDto` to mark a class for Phase 2e's
16
- * doc-merger. The marker carries `{ __zodNestDto: true, dtoId, io }` so the
17
- * merger can locate every zod-nest DTO in the @nestjs/swagger document and
18
- * inject the real Zod-derived schema.
15
+ * OpenAPI extension key used by `createZodDto` to mark a class for `applyZodNest`.
16
+ * The marker carries `{ __zodNestDto: true, dtoId, io }` so `applyZodNest` can
17
+ * locate every zod-nest DTO in the @nestjs/swagger document and inject the
18
+ * real Zod-derived schema.
19
19
  */
20
20
  declare const ZOD_NEST_DTO_EXTENSION = "x-zod-nest-dto";
21
21
 
@@ -34,21 +34,50 @@ interface ZodNestRegistry {
34
34
  hasCollision(id: string): boolean;
35
35
  getCollisions(): ReadonlyMap<string, ReadonlySet<z.ZodType>>;
36
36
  /**
37
- * Snapshot of every id registered through this `ZodNestRegistry`. Phase 2e's
38
- * bulk-mode emitter uses it to filter `z.toJSONSchema(registry.zodRegistry,
39
- * ...)` output to zod-nest-known ids only (the underlying Zod registry is
40
- * `z.globalRegistry`, which can hold third-party entries).
37
+ * Snapshot of every id registered through this `ZodNestRegistry`. The
38
+ * underlying Zod registry is `z.globalRegistry`, which may hold third-party
39
+ * entries bulk emission filters its output against this snapshot to keep
40
+ * only zod-nest-known ids.
41
41
  */
42
42
  ids(): readonly string[];
43
43
  }
44
44
  declare const createRegistry: () => ZodNestRegistry;
45
+ /** Process-wide default registry, used when no explicit `options.registry` is passed. */
46
+ declare const defaultRegistry: ZodNestRegistry;
47
+
45
48
  /**
46
- * Process-wide default registry. `createZodDto` registers schemas here unless
47
- * the caller passes `options.registry`. Phase 2e's doc merger reads from the
48
- * same instance for bulk emission. Multi-app WeakMap isolation is deferred
49
- * to v0.2.
49
+ * Composition layer emits OpenAPI `allOf` for schemas derived via `extend`.
50
+ *
51
+ * **EXPERIMENTAL**: output shape may change as edge cases surface.
50
52
  */
51
- declare const defaultRegistry: ZodNestRegistry;
53
+
54
+ /**
55
+ * Lineage record for a composition-derived schema. Read by the override to
56
+ * emit `allOf` instead of a flat body.
57
+ *
58
+ * @experimental — shape may change.
59
+ */
60
+ interface LineageEntry {
61
+ readonly op: 'extend';
62
+ readonly parent: z.ZodObject;
63
+ }
64
+ /**
65
+ * Wraps a derived `z.ZodObject` and records the parent → child link so
66
+ * emission rewrites the body to `allOf: [{ $ref: <parent> }, <delta>]`.
67
+ *
68
+ * @experimental — output shape may change as the surface stabilizes.
69
+ *
70
+ * @example
71
+ * const Base = z.object({ id: z.string() }).meta({ id: 'Base' });
72
+ * const Child = extend(Base, (s) => s.extend({ role: z.string() }).meta({ id: 'Child' }));
73
+ */
74
+ declare const extend: <P extends z.ZodObject, S extends z.ZodObject>(parent: P, build: (p: P) => S) => S;
75
+ /**
76
+ * Read the lineage entry for a composition-derived schema, or `undefined`.
77
+ *
78
+ * @experimental — `LineageEntry` shape may change.
79
+ */
80
+ declare const getLineage: (schema: z.ZodType) => LineageEntry | undefined;
52
81
 
53
82
  interface ToOpenApiOptions {
54
83
  io: 'input' | 'output';
@@ -71,11 +100,7 @@ declare class ZodNestUnrepresentableError extends ZodNestError {
71
100
  constructor(path: ReadonlyArray<string | number>, zodType: string);
72
101
  }
73
102
 
74
- /**
75
- * Runtime detection marker placed on every class returned by `createZodDto`.
76
- * Used by the validation pipe (Phase 2c), serializer (2d), and doc merger (2e)
77
- * to discriminate zod-nest DTO classes from regular constructors.
78
- */
103
+ /** Runtime tag placed on every class returned by `createZodDto`. */
79
104
  declare const ZOD_DTO_SYMBOL: unique symbol;
80
105
 
81
106
  type Io = 'input' | 'output';
@@ -105,7 +130,7 @@ declare const createZodDto: <TSchema extends z.ZodType>(schema: TSchema, options
105
130
 
106
131
  /**
107
132
  * Payload of the `x-zod-nest-dto` placeholder property that
108
- * `_OPENAPI_METADATA_FACTORY` returns. Phase 2e's doc merger reads this off
133
+ * `_OPENAPI_METADATA_FACTORY` returns. `applyZodNest` reads this off
109
134
  * each `components.schemas.<DtoName>.properties[x-zod-nest-dto]` entry,
110
135
  * uses `dtoId` to look up the schema in the registry, and replaces the
111
136
  * synthetic schema body with the real Zod-derived schema.
@@ -113,7 +138,7 @@ declare const createZodDto: <TSchema extends z.ZodType>(schema: TSchema, options
113
138
  * `type` and `required` are benign filler that satisfy @nestjs/swagger's
114
139
  * property-type guard (without them, the explorer throws "A circular
115
140
  * dependency has been detected"). They have no semantic meaning and are
116
- * stripped along with the rest of the marker by Phase 2e.
141
+ * stripped along with the rest of the marker by `applyZodNest`.
117
142
  */
118
143
  interface ZodDtoMarker {
119
144
  readonly type: () => typeof Object;
@@ -123,14 +148,13 @@ interface ZodDtoMarker {
123
148
  readonly io: Io;
124
149
  }
125
150
  declare const makeZodDtoMarker: (dtoId: string, io: Io) => ZodDtoMarker;
126
- /** Phase 2e (and tests) use this to discriminate a marker from a real schema. */
127
151
  declare const isZodDtoMarker: (value: unknown) => value is ZodDtoMarker;
128
152
 
129
153
  /**
130
154
  * Runtime guard: is `value` a class returned by `createZodDto`?
131
155
  *
132
- * Used by the validation pipe (Phase 2c), the serializer interceptor (2d),
133
- * and the doc merger (2e) to discriminate zod-nest DTOs from plain
156
+ * Used by `ZodValidationPipe`, `ZodSerializerInterceptor`, and `applyZodNest`
157
+ * to discriminate zod-nest DTOs from plain
134
158
  * constructors, class-validator DTOs, primitives, and any other metatypes
135
159
  * NestJS exposes via `ArgumentMetadata`.
136
160
  */
@@ -146,13 +170,18 @@ declare const isZodDto: (value: unknown) => value is ZodDto;
146
170
  * {
147
171
  * statusCode: 500,
148
172
  * message: 'Response validation failed',
149
- * errors: z.treeifyError(zodError),
150
173
  * }
151
174
  * ```
152
175
  *
153
- * Carries `zodError` and `executionContext` so custom exception filters
154
- * can introspect the original validation failure and the request that
155
- * produced it.
176
+ * The zod error tree is **deliberately not exposed in the response body** —
177
+ * a serialization failure is a server-side contract violation, and leaking
178
+ * the schema-shape error tree to clients discloses internal structure. The
179
+ * full treeified error is logged through `ZodNestModule`'s validation-log
180
+ * channel (with redaction + truncation) so operators get the diagnostic
181
+ * information without it reaching the wire.
182
+ *
183
+ * Custom exception filters can still introspect the failure: `zodError` and
184
+ * `executionContext` are kept as own properties on the instance.
156
185
  */
157
186
  declare class ZodSerializationException extends InternalServerErrorException {
158
187
  readonly zodError: z.ZodError;
@@ -294,7 +323,7 @@ declare const ZOD_RESPONSES_METADATA_KEY: unique symbol;
294
323
  type ResponseVariantKind = 'single' | 'array' | 'tuple';
295
324
  /**
296
325
  * Description payload accepted by `@ZodResponse(...)` and passed through to
297
- * Phase 2e's `@ApiResponse(...)` emitter. String form is shorthand for
326
+ * `applyZodNest`'s `@ApiResponse(...)` emitter. String form is shorthand for
298
327
  * `{ description }`; the object form lets users declare OpenAPI response
299
328
  * `headers` / `links` alongside the description.
300
329
  */
@@ -305,8 +334,8 @@ type ZodResponseDescription = string | {
305
334
  };
306
335
  /**
307
336
  * One variant record per `@ZodResponse(...)` call. `dto` is kept alongside
308
- * `validationSchema` so Phase 2e can emit `@ApiResponse({ type })` without
309
- * unwrapping the runtime-only `z.array(...)` / `z.tuple([...])` wrapper.
337
+ * `validationSchema` so `applyZodNest` can emit `@ApiResponse({ type })`
338
+ * without unwrapping the runtime-only `z.array(...)` / `z.tuple([...])` wrapper.
310
339
  *
311
340
  * `status` is `undefined` when the user didn't pass one explicitly — the
312
341
  * effective status is resolved lazily by `resolveEffectiveStatus(variant,
@@ -330,7 +359,7 @@ interface ResponseVariant {
330
359
  * `@ApiResponse({ isArray: true })` convention without minting a separate
331
360
  * `*sDto` id.
332
361
  * - `[A, B, ...]` (length ≥ 2) → validates as `z.tuple([A.schema, B.schema, ...])`;
333
- * surfaces as an OpenAPI 3.1 `prefixItems` tuple in Phase 2e.
362
+ * surfaces as an OpenAPI 3.1 `prefixItems` tuple via `applyZodNest`.
334
363
  *
335
364
  * Empty arrays and non-DTO elements throw `TypeError` at decoration time
336
365
  * so typos surface at module load, not the first request.
@@ -417,8 +446,7 @@ interface ApplyZodNestOptions {
417
446
  /**
418
447
  * `ZodNestRegistry` instance that holds the zod-nest DTOs. Defaults to
419
448
  * `defaultRegistry` (the process-wide singleton populated by `createZodDto`).
420
- * Pass an explicit registry for multi-app isolation (planned formalization
421
- * in v0.2).
449
+ * Pass an explicit registry for multi-app isolation.
422
450
  */
423
451
  registry?: ZodNestRegistry;
424
452
  /** User override pipe applied on top of the built-in override during emission. */
@@ -470,4 +498,4 @@ declare class ZodNestDocumentError extends ZodNestError {
470
498
  constructor(code: ZodNestDocumentErrorCode, message: string, details?: Record<string, unknown>);
471
499
  }
472
500
 
473
- export { type ApplyZodNestOptions, COMPONENTS_SCHEMAS_PREFIX, type CreateSerializationException, type CreateValidationException, type CreateZodDtoOptions, DEFAULT_MAX_LOGGED_VALUE_BYTES, DEFAULT_REDACT_KEYS, type Io, type NormalizedZodNestOptions, type Override, type OverrideContext, type ResponseVariant, type ResponseVariantKind, type 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, type ZodDto, type ZodDtoMarker, ZodNestDocumentError, type ZodNestDocumentErrorCode, ZodNestError, ZodNestModule, type ZodNestModuleOptions, type ZodNestRegistry, ZodNestUnrepresentableError, ZodResponse, type ZodResponseDescription, type ZodResponseOptions, type ZodResponseType, ZodSerializationException, ZodSerializerInterceptor, ZodValidationException, ZodValidationPipe, type ZodValidationPipeArg, type ZodValidationPipeOptions, applyZodNest, createRegistry, createZodDto, defaultRegistry, defaultStatusFor, isZodDto, isZodDtoMarker, makeZodDtoMarker, resolveEffectiveStatus, toOpenApi };
501
+ 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 ResponseVariant, type ResponseVariantKind, type 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, type ZodDto, type ZodDtoMarker, ZodNestDocumentError, type ZodNestDocumentErrorCode, ZodNestError, ZodNestModule, type ZodNestModuleOptions, type ZodNestRegistry, ZodNestUnrepresentableError, 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, resolveEffectiveStatus, toOpenApi };