zod-nest 0.7.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 +474 -0
- package/dist/index.d.mts +44 -50
- package/dist/index.d.ts +44 -50
- package/dist/index.js +97 -63
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +97 -63
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -2
package/README.md
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
# zod-nest
|
|
2
|
+
|
|
3
|
+
> Modern **Zod v4** ↔ **NestJS** ↔ **OpenAPI 3.1** integration.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/zod-nest)
|
|
6
|
+
[](https://github.com/rodrigowbazevedo/zod-nest/actions/workflows/ci.yml)
|
|
7
|
+
[](https://codecov.io/gh/rodrigowbazevedo/zod-nest)
|
|
8
|
+
[](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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
|
@@ -28,17 +28,34 @@ interface OverrideContext {
|
|
|
28
28
|
}
|
|
29
29
|
type Override = (ctx: OverrideContext) => void;
|
|
30
30
|
|
|
31
|
+
interface ZodNestRegistry {
|
|
32
|
+
readonly zodRegistry: typeof z.globalRegistry;
|
|
33
|
+
register(schema: z.ZodType, id: string): void;
|
|
34
|
+
hasCollision(id: string): boolean;
|
|
35
|
+
getCollisions(): ReadonlyMap<string, ReadonlySet<z.ZodType>>;
|
|
36
|
+
/**
|
|
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
|
+
*/
|
|
42
|
+
ids(): readonly string[];
|
|
43
|
+
}
|
|
44
|
+
declare const createRegistry: () => ZodNestRegistry;
|
|
45
|
+
/** Process-wide default registry, used when no explicit `options.registry` is passed. */
|
|
46
|
+
declare const defaultRegistry: ZodNestRegistry;
|
|
47
|
+
|
|
31
48
|
/**
|
|
32
49
|
* Composition layer — emits OpenAPI `allOf` for schemas derived via `extend`.
|
|
33
50
|
*
|
|
34
|
-
* **EXPERIMENTAL
|
|
51
|
+
* **EXPERIMENTAL**: output shape may change as edge cases surface.
|
|
35
52
|
*/
|
|
36
53
|
|
|
37
54
|
/**
|
|
38
55
|
* Lineage record for a composition-derived schema. Read by the override to
|
|
39
56
|
* emit `allOf` instead of a flat body.
|
|
40
57
|
*
|
|
41
|
-
* @experimental
|
|
58
|
+
* @experimental — shape may change.
|
|
42
59
|
*/
|
|
43
60
|
interface LineageEntry {
|
|
44
61
|
readonly op: 'extend';
|
|
@@ -48,7 +65,7 @@ interface LineageEntry {
|
|
|
48
65
|
* Wraps a derived `z.ZodObject` and records the parent → child link so
|
|
49
66
|
* emission rewrites the body to `allOf: [{ $ref: <parent> }, <delta>]`.
|
|
50
67
|
*
|
|
51
|
-
* @experimental
|
|
68
|
+
* @experimental — output shape may change as the surface stabilizes.
|
|
52
69
|
*
|
|
53
70
|
* @example
|
|
54
71
|
* const Base = z.object({ id: z.string() }).meta({ id: 'Base' });
|
|
@@ -58,32 +75,10 @@ declare const extend: <P extends z.ZodObject, S extends z.ZodObject>(parent: P,
|
|
|
58
75
|
/**
|
|
59
76
|
* Read the lineage entry for a composition-derived schema, or `undefined`.
|
|
60
77
|
*
|
|
61
|
-
* @experimental
|
|
78
|
+
* @experimental — `LineageEntry` shape may change.
|
|
62
79
|
*/
|
|
63
80
|
declare const getLineage: (schema: z.ZodType) => LineageEntry | undefined;
|
|
64
81
|
|
|
65
|
-
interface ZodNestRegistry {
|
|
66
|
-
readonly zodRegistry: typeof z.globalRegistry;
|
|
67
|
-
register(schema: z.ZodType, id: string): void;
|
|
68
|
-
hasCollision(id: string): boolean;
|
|
69
|
-
getCollisions(): ReadonlyMap<string, ReadonlySet<z.ZodType>>;
|
|
70
|
-
/**
|
|
71
|
-
* Snapshot of every id registered through this `ZodNestRegistry`. Phase 2e's
|
|
72
|
-
* bulk-mode emitter uses it to filter `z.toJSONSchema(registry.zodRegistry,
|
|
73
|
-
* ...)` output to zod-nest-known ids only (the underlying Zod registry is
|
|
74
|
-
* `z.globalRegistry`, which can hold third-party entries).
|
|
75
|
-
*/
|
|
76
|
-
ids(): readonly string[];
|
|
77
|
-
}
|
|
78
|
-
declare const createRegistry: () => ZodNestRegistry;
|
|
79
|
-
/**
|
|
80
|
-
* Process-wide default registry. `createZodDto` registers schemas here unless
|
|
81
|
-
* the caller passes `options.registry`. Phase 2e's doc merger reads from the
|
|
82
|
-
* same instance for bulk emission. Multi-app WeakMap isolation is deferred
|
|
83
|
-
* to v0.2.
|
|
84
|
-
*/
|
|
85
|
-
declare const defaultRegistry: ZodNestRegistry;
|
|
86
|
-
|
|
87
82
|
interface ToOpenApiOptions {
|
|
88
83
|
io: 'input' | 'output';
|
|
89
84
|
registry: ZodNestRegistry;
|
|
@@ -105,11 +100,7 @@ declare class ZodNestUnrepresentableError extends ZodNestError {
|
|
|
105
100
|
constructor(path: ReadonlyArray<string | number>, zodType: string);
|
|
106
101
|
}
|
|
107
102
|
|
|
108
|
-
/**
|
|
109
|
-
* Runtime detection marker placed on every class returned by `createZodDto`.
|
|
110
|
-
* Used by the validation pipe (Phase 2c), serializer (2d), and doc merger (2e)
|
|
111
|
-
* to discriminate zod-nest DTO classes from regular constructors.
|
|
112
|
-
*/
|
|
103
|
+
/** Runtime tag placed on every class returned by `createZodDto`. */
|
|
113
104
|
declare const ZOD_DTO_SYMBOL: unique symbol;
|
|
114
105
|
|
|
115
106
|
type Io = 'input' | 'output';
|
|
@@ -139,7 +130,7 @@ declare const createZodDto: <TSchema extends z.ZodType>(schema: TSchema, options
|
|
|
139
130
|
|
|
140
131
|
/**
|
|
141
132
|
* Payload of the `x-zod-nest-dto` placeholder property that
|
|
142
|
-
* `_OPENAPI_METADATA_FACTORY` returns.
|
|
133
|
+
* `_OPENAPI_METADATA_FACTORY` returns. `applyZodNest` reads this off
|
|
143
134
|
* each `components.schemas.<DtoName>.properties[x-zod-nest-dto]` entry,
|
|
144
135
|
* uses `dtoId` to look up the schema in the registry, and replaces the
|
|
145
136
|
* synthetic schema body with the real Zod-derived schema.
|
|
@@ -147,7 +138,7 @@ declare const createZodDto: <TSchema extends z.ZodType>(schema: TSchema, options
|
|
|
147
138
|
* `type` and `required` are benign filler that satisfy @nestjs/swagger's
|
|
148
139
|
* property-type guard (without them, the explorer throws "A circular
|
|
149
140
|
* dependency has been detected"). They have no semantic meaning and are
|
|
150
|
-
* stripped along with the rest of the marker by
|
|
141
|
+
* stripped along with the rest of the marker by `applyZodNest`.
|
|
151
142
|
*/
|
|
152
143
|
interface ZodDtoMarker {
|
|
153
144
|
readonly type: () => typeof Object;
|
|
@@ -157,14 +148,13 @@ interface ZodDtoMarker {
|
|
|
157
148
|
readonly io: Io;
|
|
158
149
|
}
|
|
159
150
|
declare const makeZodDtoMarker: (dtoId: string, io: Io) => ZodDtoMarker;
|
|
160
|
-
/** Phase 2e (and tests) use this to discriminate a marker from a real schema. */
|
|
161
151
|
declare const isZodDtoMarker: (value: unknown) => value is ZodDtoMarker;
|
|
162
152
|
|
|
163
153
|
/**
|
|
164
154
|
* Runtime guard: is `value` a class returned by `createZodDto`?
|
|
165
155
|
*
|
|
166
|
-
* Used by
|
|
167
|
-
*
|
|
156
|
+
* Used by `ZodValidationPipe`, `ZodSerializerInterceptor`, and `applyZodNest`
|
|
157
|
+
* to discriminate zod-nest DTOs from plain
|
|
168
158
|
* constructors, class-validator DTOs, primitives, and any other metatypes
|
|
169
159
|
* NestJS exposes via `ArgumentMetadata`.
|
|
170
160
|
*/
|
|
@@ -180,13 +170,18 @@ declare const isZodDto: (value: unknown) => value is ZodDto;
|
|
|
180
170
|
* {
|
|
181
171
|
* statusCode: 500,
|
|
182
172
|
* message: 'Response validation failed',
|
|
183
|
-
* errors: z.treeifyError(zodError),
|
|
184
173
|
* }
|
|
185
174
|
* ```
|
|
186
175
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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.
|
|
190
185
|
*/
|
|
191
186
|
declare class ZodSerializationException extends InternalServerErrorException {
|
|
192
187
|
readonly zodError: z.ZodError;
|
|
@@ -328,7 +323,7 @@ declare const ZOD_RESPONSES_METADATA_KEY: unique symbol;
|
|
|
328
323
|
type ResponseVariantKind = 'single' | 'array' | 'tuple';
|
|
329
324
|
/**
|
|
330
325
|
* Description payload accepted by `@ZodResponse(...)` and passed through to
|
|
331
|
-
*
|
|
326
|
+
* `applyZodNest`'s `@ApiResponse(...)` emitter. String form is shorthand for
|
|
332
327
|
* `{ description }`; the object form lets users declare OpenAPI response
|
|
333
328
|
* `headers` / `links` alongside the description.
|
|
334
329
|
*/
|
|
@@ -339,8 +334,8 @@ type ZodResponseDescription = string | {
|
|
|
339
334
|
};
|
|
340
335
|
/**
|
|
341
336
|
* One variant record per `@ZodResponse(...)` call. `dto` is kept alongside
|
|
342
|
-
* `validationSchema` so
|
|
343
|
-
* 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.
|
|
344
339
|
*
|
|
345
340
|
* `status` is `undefined` when the user didn't pass one explicitly — the
|
|
346
341
|
* effective status is resolved lazily by `resolveEffectiveStatus(variant,
|
|
@@ -364,7 +359,7 @@ interface ResponseVariant {
|
|
|
364
359
|
* `@ApiResponse({ isArray: true })` convention without minting a separate
|
|
365
360
|
* `*sDto` id.
|
|
366
361
|
* - `[A, B, ...]` (length ≥ 2) → validates as `z.tuple([A.schema, B.schema, ...])`;
|
|
367
|
-
* surfaces as an OpenAPI 3.1 `prefixItems` tuple
|
|
362
|
+
* surfaces as an OpenAPI 3.1 `prefixItems` tuple via `applyZodNest`.
|
|
368
363
|
*
|
|
369
364
|
* Empty arrays and non-DTO elements throw `TypeError` at decoration time
|
|
370
365
|
* so typos surface at module load, not the first request.
|
|
@@ -451,8 +446,7 @@ interface ApplyZodNestOptions {
|
|
|
451
446
|
/**
|
|
452
447
|
* `ZodNestRegistry` instance that holds the zod-nest DTOs. Defaults to
|
|
453
448
|
* `defaultRegistry` (the process-wide singleton populated by `createZodDto`).
|
|
454
|
-
* Pass an explicit registry for multi-app isolation
|
|
455
|
-
* in v0.2).
|
|
449
|
+
* Pass an explicit registry for multi-app isolation.
|
|
456
450
|
*/
|
|
457
451
|
registry?: ZodNestRegistry;
|
|
458
452
|
/** User override pipe applied on top of the built-in override during emission. */
|