@voznov/zod-dto-nestjs 0.2.2 → 0.3.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 +124 -5
- package/dist/index.cjs +1191 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +1197 -9
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -19,11 +19,16 @@ app.useGlobalPipes(new ZodValidationPipe());
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
|
-
import { Body, Controller, Post } from '@nestjs/common';
|
|
22
|
+
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
|
|
23
23
|
import { ZodDto } from '@voznov/zod-dto';
|
|
24
24
|
import { z } from 'zod';
|
|
25
25
|
|
|
26
26
|
class CreateUserDto extends ZodDto(z.object({ name: z.string(), email: z.email() })) {}
|
|
27
|
+
class UserIdParam extends ZodDto(z.object({ id: z.uuid() })) {}
|
|
28
|
+
class ListUsersQuery extends ZodDto(z.object({
|
|
29
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
30
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
31
|
+
})) {}
|
|
27
32
|
|
|
28
33
|
@Controller('users')
|
|
29
34
|
export class UsersController {
|
|
@@ -31,6 +36,16 @@ export class UsersController {
|
|
|
31
36
|
create(@Body() body: CreateUserDto) {
|
|
32
37
|
// already validated; `body` is a CreateUserDto instance.
|
|
33
38
|
}
|
|
39
|
+
|
|
40
|
+
// `@Param() params: UserIdParam` — `id` validated as UUID, format: 'uuid' in the spec.
|
|
41
|
+
// (Cleaner than `@Param('id', ParseUUIDPipe) id: string`, and the spec carries the format.)
|
|
42
|
+
@Get(':id')
|
|
43
|
+
findOne(@Param() params: UserIdParam) { /* ... */ }
|
|
44
|
+
|
|
45
|
+
// `@Query() query: ListUsersQuery` — every Zod field becomes one OpenAPI query parameter,
|
|
46
|
+
// with per-field validation, defaults, and descriptions, no extra `@ApiQuery` decorators needed.
|
|
47
|
+
@Get()
|
|
48
|
+
list(@Query() query: ListUsersQuery) { /* ... */ }
|
|
34
49
|
}
|
|
35
50
|
```
|
|
36
51
|
|
|
@@ -52,6 +67,20 @@ Supported shapes: scalars, objects (nested), arrays, tuples, records, enums, lit
|
|
|
52
67
|
|
|
53
68
|
`.refine(...)` validators run at request-validation time but are **not** reflected in the spec — JSON Schema can't express custom predicates, and a single `description` for a chain of refines (`.refine(...).refine(...).refine(...)`) would be ambiguous. Put human-readable docs in `.describe(...)` instead.
|
|
54
69
|
|
|
70
|
+
> ⚠️ **`in` / `out` hooks are runtime-only.** The walker only reads the schema's structure, not the options passed to `ZodDto(schema, { in, out })`. A `out: ({ password, ...rest }) => rest` correctly strips `password` from the response *body*, but the OpenAPI schema still lists `password` as a property — the spec lies about a field that runtime drops. Same for `in`: snake_case→camelCase aliases applied via `in` are invisible in the spec, so docs show only the camelCase shape. If spec-correctness matters, either omit the field from the schema itself (`schema.omit({ password: true })`) or maintain a separate response DTO.
|
|
71
|
+
|
|
72
|
+
### Reference nested DTOs by class, not by raw schema
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// ❌ Inlines NoteDto's full shape into items[]; codegen produces two separate types for the same data.
|
|
76
|
+
class PaginatedNotes extends ZodDto(z.object({ items: z.array(noteSchema) })) {}
|
|
77
|
+
|
|
78
|
+
// ✅ Emits `items: { type: 'array', items: { $ref: '#/components/schemas/NoteDto' } }`.
|
|
79
|
+
class PaginatedNotes extends ZodDto(z.object({ items: z.array(NoteDto) })) {}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Both forms parse identically, but only the second one keeps the spec DRY — `$ref` instead of an inlined copy. Use the DTO class itself in nested positions whenever you have one.
|
|
83
|
+
|
|
55
84
|
### Recursive schemas (`z.lazy` / `lazyDto`)
|
|
56
85
|
|
|
57
86
|
For self-referential shapes (comment trees, file trees, ...) wrap the recursion in a DTO and reference it back via `lazyDto` — the Swagger walker emits a proper `$ref` at the cycle, and `lazyDto` keeps TypeScript from tripping over the circular self-reference:
|
|
@@ -88,12 +117,102 @@ app.useGlobalPipes(
|
|
|
88
117
|
);
|
|
89
118
|
```
|
|
90
119
|
|
|
120
|
+
## Response validation — `@ZodSerialize` / `@ZodResponse`
|
|
121
|
+
|
|
122
|
+
Method decorators that parse the return value of a controller (or any class) method through a Zod schema. If the method returns something that doesn't match the schema, a `ZodDtoSerializationError` is thrown — caught at runtime *before* the value reaches the client, so server-side bugs are surfaced as 500s instead of leaking malformed payloads or extra fields.
|
|
123
|
+
|
|
124
|
+
- **`@ZodSerialize`** — runtime parsing only. Use on services, repositories, internal methods.
|
|
125
|
+
- **`@ZodResponse`** — `@ZodSerialize` + auto-emit `@ApiResponse` Swagger metadata (and register inner DTOs via `@ApiExtraModels`). Use on controller routes.
|
|
126
|
+
|
|
127
|
+
Both come in two overloads:
|
|
128
|
+
|
|
129
|
+
- **Strict** — schema passed explicitly. The method's return type is constrained at compile time to match the schema's output; `tsc` errors on mismatch.
|
|
130
|
+
- **Loose** — no schema. Resolves from `design:returntype` metadata at runtime (`: NoteDto` annotation suffices). No compile-time check; doesn't work on generic return types (`NoteDto[]`, `Promise<NoteDto>`, unions) since TypeScript erases generics in metadata — pass the schema explicitly in that case.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
|
134
|
+
import { ZodResponse } from '@voznov/zod-dto-nestjs';
|
|
135
|
+
import { z } from 'zod';
|
|
136
|
+
|
|
137
|
+
@Controller('notes')
|
|
138
|
+
export class NotesController {
|
|
139
|
+
// Strict + auto-Swagger: tsc enforces the return type, spec gets `$ref` to NoteDto.
|
|
140
|
+
@Get(':id')
|
|
141
|
+
@ZodResponse(NoteDto)
|
|
142
|
+
findOne(@Param() p: NoteIdParam): NoteDto { /* ... */ }
|
|
143
|
+
|
|
144
|
+
// Async + array: tsc enforces `Promise<NoteDto[]>`. Generics erased in design:returntype,
|
|
145
|
+
// so the schema must be passed explicitly here.
|
|
146
|
+
@Get()
|
|
147
|
+
@ZodResponse(z.array(NoteDto))
|
|
148
|
+
async list(): Promise<NoteDto[]> { /* ... */ }
|
|
149
|
+
|
|
150
|
+
// Override status (default 200) + description for the OpenAPI operation.
|
|
151
|
+
@Post()
|
|
152
|
+
@ZodResponse(NoteDto, { status: 201, description: 'note created' })
|
|
153
|
+
create(@Body() body: CreateNoteDto): NoteDto { /* ... */ }
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Runtime-only sibling for layers below the controller — same overloads, no Swagger emission:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { ZodSerialize } from '@voznov/zod-dto-nestjs';
|
|
161
|
+
|
|
162
|
+
class NotesService {
|
|
163
|
+
// Throws ZodDtoSerializationError if the return shape doesn't match.
|
|
164
|
+
@ZodSerialize(NoteDto)
|
|
165
|
+
findOne(id: string): NoteDto { /* ... */ }
|
|
166
|
+
|
|
167
|
+
// Loose: schema resolved from `design:returntype` (NoteDto class). Won't work for `Promise<...>` / `NoteDto[]` — generic erased to `Promise` / `Array`. Use the strict overload for those.
|
|
168
|
+
@ZodSerialize()
|
|
169
|
+
default(): NoteDto { /* ... */ }
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Options
|
|
174
|
+
|
|
175
|
+
Both decorators accept the full `ToDtoOptions` bag (`preprocessors`, `observers`, `errorClass` — same semantics as [`toDto.with`](https://www.npmjs.com/package/@voznov/zod-dto), but applied to the method's *return* value instead of an input). The default `errorClass` is `ZodDtoSerializationError` (vs `ZodDtoValidationError` for `toDto`), so an exception filter can split client errors from server bugs (see below).
|
|
176
|
+
|
|
177
|
+
`@ZodResponse` extends the bag with two Swagger-only fields:
|
|
178
|
+
|
|
179
|
+
- `status: number` — HTTP status for the OpenAPI response object. Default `200`.
|
|
180
|
+
- `description: string` — description on the OpenAPI response object.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
@ZodResponse(NoteDto, { status: 201, observers: [(note) => metrics.recordCreate(note)] })
|
|
184
|
+
async create(): Promise<NoteDto> { /* ... */ }
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Differentiating client errors from server errors
|
|
188
|
+
|
|
189
|
+
`ZodDtoSerializationError extends ZodDtoValidationError`, so a single exception filter can split request-validation failures (client → 400) from response-validation failures (server → 500):
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { ZodDtoValidationError } from '@voznov/zod-dto';
|
|
193
|
+
import { ZodDtoSerializationError } from '@voznov/zod-dto-nestjs';
|
|
194
|
+
|
|
195
|
+
@Catch(ZodDtoValidationError)
|
|
196
|
+
export class ZodExceptionFilter implements ExceptionFilter {
|
|
197
|
+
catch(error: ZodDtoValidationError, host: ArgumentsHost) {
|
|
198
|
+
const isServerBug = error instanceof ZodDtoSerializationError;
|
|
199
|
+
const status = isServerBug ? 500 : 400;
|
|
200
|
+
// log, format, respond...
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
91
205
|
## API
|
|
92
206
|
|
|
93
|
-
| Export
|
|
94
|
-
|
|
|
95
|
-
| `ZodValidationPipe`
|
|
96
|
-
| `
|
|
207
|
+
| Export | Description |
|
|
208
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
209
|
+
| `ZodValidationPipe` | `PipeTransform` for `@Body()` / `@Param()` / `@Query()`. Accepts `{ createError?: (issues: string[]) => Error }`. |
|
|
210
|
+
| `ZodValidationPipeOptions` | Options type for `ZodValidationPipe`. |
|
|
211
|
+
| `ZodSerialize(schema?, options?)` | Method decorator: runtime-parse the return value through `schema` (or via `design:returntype` if omitted). Throws `ZodDtoSerializationError` on mismatch. |
|
|
212
|
+
| `ZodResponse(schema?, options?)` | `ZodSerialize` + auto-emits `@ApiResponse` Swagger metadata (and `@ApiExtraModels` for inner DTOs). |
|
|
213
|
+
| `ZodResponseOptions` | Options type for `ZodResponse` (`ToDtoOptions & { status?, description? }`). |
|
|
214
|
+
| `ZodDtoSerializationError` | Subclass of `ZodDtoValidationError` thrown by `@ZodSerialize` / `@ZodResponse` when a method returns an invalid shape. |
|
|
215
|
+
| `applySwaggerDecorators(schema)` | Low-level: apply `@ApiProperty` metadata to a schema. Auto-invoked via `registerOnCreate`; export is for manual/edge-case use. |
|
|
97
216
|
|
|
98
217
|
## License
|
|
99
218
|
|