zod-nest 0.7.0 → 0.8.2

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,489 @@
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
+ ## AI tooling — `npx skills`
465
+
466
+ `zod-nest` ships two AI-agent skills you can install into your project via [`npx skills`](https://github.com/vercel-labs/skills) (Claude Code primary; Cursor / Continue best-effort):
467
+
468
+ - **`zod-nest-migrate`** — walks an agent through the 8-step `nestjs-zod` → `zod-nest` migration, plan-then-apply per step.
469
+ - **`zod-nest`** — diagnostic best-practices skill for schema and `@ZodResponse` ergonomics; auto-triggers on edits to `*.controller.ts` / `*.dto.ts` files that import from `zod-nest`.
470
+
471
+ ```bash
472
+ npx skills add rodrigowbazevedo/zod-nest # both skills
473
+ npx skills add rodrigowbazevedo/zod-nest --skill zod-nest-migrate # migration only
474
+ npx skills add rodrigowbazevedo/zod-nest --skill zod-nest # best-practices only
475
+ ```
476
+
477
+ Full details, agent compatibility notes, and what each skill diagnoses: [`docs/skills.md`](docs/skills.md).
478
+
479
+ ## Contributing
480
+
481
+ `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.
482
+
483
+ 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).
484
+
485
+ ## License
486
+
487
+ MIT — see [`LICENSE`](LICENSE).
488
+
489
+ 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).