zod-nest 3.2.6 → 3.4.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 +4 -4
- package/dist/index.d.mts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +122 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +122 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# zod-nest
|
|
2
2
|
|
|
3
|
-
> Modern **Zod v4** ↔ **NestJS** ↔ **OpenAPI 3.1** integration.
|
|
3
|
+
> Modern **Zod v4** ↔ **NestJS** ↔ **OpenAPI 3.1 / 3.2** integration.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/zod-nest)
|
|
6
6
|
[](https://github.com/rodrigowbazevedo/zod-nest/actions/workflows/ci.yml)
|
|
7
7
|
[](https://codecov.io/gh/rodrigowbazevedo/zod-nest)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
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.
|
|
10
|
+
Define your DTOs once with Zod, get validated request bodies, validated response bodies, and a correct OpenAPI 3.1 or 3.2 document — without the dual-codepath, post-process, or `@ts-ignore` baggage that comes with bolting Zod onto class-validator-shaped tooling.
|
|
11
11
|
|
|
12
12
|
## Why this exists
|
|
13
13
|
|
|
14
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
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.
|
|
16
|
+
The difference is that `zod-nest` is Zod v4 only and OpenAPI 3.1+ only (3.1 or 3.2; no 3.0). 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
17
|
|
|
18
18
|
For the long-form motivation, see [`docs/why-this-exists.md`](docs/why-this-exists.md).
|
|
19
19
|
|
|
@@ -42,7 +42,7 @@ A short list of behavioural differences you'll hit on day one. Full migration ta
|
|
|
42
42
|
- **Multi-status `@ZodResponse`** — stack the decorator per status code. In `nestjs-zod`, multi-status required mixing `@ZodSerializerDto` with hand-rolled `@ApiResponse({ status: ... })` calls.
|
|
43
43
|
- **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. `status` accepts numeric codes plus the OpenAPI 3.1 range keys (`'1XX'`…`'5XX'`) and `'default'` (sugar for the resolved method default).
|
|
44
44
|
- **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`.
|
|
45
|
-
- **OpenAPI 3.1
|
|
45
|
+
- **OpenAPI 3.1 and 3.2** — no `3.0` fallback. Pick with `DocumentBuilder.setOpenAPIVersion('3.2.0')`; 3.2 is what makes `QUERY` routes conformant. `$ref`s emit to the final location; `cleanupOpenApiDoc` is unnecessary.
|
|
46
46
|
- **Validation-failure logging out of the box** — `nestjs-zod` has none.
|
|
47
47
|
- **Customizable serialization exception** — both `ZodValidationPipe` and `ZodSerializerInterceptor` accept a factory. `nestjs-zod` only customized the input side.
|
|
48
48
|
- **DTO discriminator** — `Symbol.for('zod-nest.dto')` (cross-realm safe), not `MyDto.isZodDto`.
|
package/dist/index.d.mts
CHANGED
|
@@ -807,9 +807,11 @@ interface ApplyZodNestOptions {
|
|
|
807
807
|
* as a `{ $ref, title }` sibling (`applyRefTitles`, unless `refTitles: false`)
|
|
808
808
|
* so Swagger UI's 3.1 renderer surfaces the component name. Inert annotation.
|
|
809
809
|
* - Every `$ref` whose target is missing throws `ZodNestDocumentError(DANGLING_REF)`.
|
|
810
|
-
* - `doc.openapi` is
|
|
811
|
-
*
|
|
812
|
-
*
|
|
810
|
+
* - `doc.openapi` is normalised to a version zod-nest emits — the one set via
|
|
811
|
+
* `DocumentBuilder.setOpenAPIVersion()` when supported, else `'3.1.0'` with a
|
|
812
|
+
* warning, so the version string always matches the emitted body.
|
|
813
|
+
* - Targeting 3.2 additionally moves `search` / WebDAV operations under
|
|
814
|
+
* `additionalOperations`, the only place that version accepts them.
|
|
813
815
|
*
|
|
814
816
|
* Composable with other doc-transform passes — apply other mutations before
|
|
815
817
|
* or after this function.
|
package/dist/index.d.ts
CHANGED
|
@@ -807,9 +807,11 @@ interface ApplyZodNestOptions {
|
|
|
807
807
|
* as a `{ $ref, title }` sibling (`applyRefTitles`, unless `refTitles: false`)
|
|
808
808
|
* so Swagger UI's 3.1 renderer surfaces the component name. Inert annotation.
|
|
809
809
|
* - Every `$ref` whose target is missing throws `ZodNestDocumentError(DANGLING_REF)`.
|
|
810
|
-
* - `doc.openapi` is
|
|
811
|
-
*
|
|
812
|
-
*
|
|
810
|
+
* - `doc.openapi` is normalised to a version zod-nest emits — the one set via
|
|
811
|
+
* `DocumentBuilder.setOpenAPIVersion()` when supported, else `'3.1.0'` with a
|
|
812
|
+
* warning, so the version string always matches the emitted body.
|
|
813
|
+
* - Targeting 3.2 additionally moves `search` / WebDAV operations under
|
|
814
|
+
* `additionalOperations`, the only place that version accepts them.
|
|
813
815
|
*
|
|
814
816
|
* Composable with other doc-transform passes — apply other mutations before
|
|
815
817
|
* or after this function.
|
package/dist/index.js
CHANGED
|
@@ -1041,6 +1041,101 @@ var ZodNestModule = class _ZodNestModule {
|
|
|
1041
1041
|
};
|
|
1042
1042
|
}
|
|
1043
1043
|
};
|
|
1044
|
+
|
|
1045
|
+
// src/document/http-methods.ts
|
|
1046
|
+
var OPENAPI_OPERATION_KEYS = [
|
|
1047
|
+
"get",
|
|
1048
|
+
"put",
|
|
1049
|
+
"post",
|
|
1050
|
+
"delete",
|
|
1051
|
+
"options",
|
|
1052
|
+
"head",
|
|
1053
|
+
"patch",
|
|
1054
|
+
"trace",
|
|
1055
|
+
"query"
|
|
1056
|
+
];
|
|
1057
|
+
var EXTENSION_OPERATION_KEYS = [
|
|
1058
|
+
"search",
|
|
1059
|
+
"propfind",
|
|
1060
|
+
"proppatch",
|
|
1061
|
+
"mkcol",
|
|
1062
|
+
"copy",
|
|
1063
|
+
"move",
|
|
1064
|
+
"lock",
|
|
1065
|
+
"unlock"
|
|
1066
|
+
];
|
|
1067
|
+
var HTTP_METHODS = [
|
|
1068
|
+
...OPENAPI_OPERATION_KEYS,
|
|
1069
|
+
...EXTENSION_OPERATION_KEYS
|
|
1070
|
+
];
|
|
1071
|
+
var ADDITIONAL_OPERATIONS_KEY = "additionalOperations";
|
|
1072
|
+
var isRecord = /* @__PURE__ */ chunkEV5I5HGT_js.__name((value) => value !== null && typeof value === "object", "isRecord");
|
|
1073
|
+
var operationsOfPathItem = /* @__PURE__ */ chunkEV5I5HGT_js.__name((pathItem) => {
|
|
1074
|
+
const operations = [];
|
|
1075
|
+
for (const method of HTTP_METHODS) {
|
|
1076
|
+
const operation = pathItem[method];
|
|
1077
|
+
if (isRecord(operation)) {
|
|
1078
|
+
operations.push(operation);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const additional = pathItem[ADDITIONAL_OPERATIONS_KEY];
|
|
1082
|
+
if (isRecord(additional)) {
|
|
1083
|
+
for (const operation of Object.values(additional)) {
|
|
1084
|
+
if (isRecord(operation)) {
|
|
1085
|
+
operations.push(operation);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return operations;
|
|
1090
|
+
}, "operationsOfPathItem");
|
|
1091
|
+
var forEachOperation = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc, fn) => {
|
|
1092
|
+
const paths = doc.paths;
|
|
1093
|
+
if (!isRecord(paths)) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
for (const pathItem of Object.values(paths)) {
|
|
1097
|
+
if (!isRecord(pathItem)) {
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
for (const operation of operationsOfPathItem(pathItem)) {
|
|
1101
|
+
fn(operation);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}, "forEachOperation");
|
|
1105
|
+
|
|
1106
|
+
// src/document/additional-operations.ts
|
|
1107
|
+
var isRecord2 = /* @__PURE__ */ chunkEV5I5HGT_js.__name((value) => value !== null && typeof value === "object", "isRecord");
|
|
1108
|
+
var relocateExtensionOperations = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc) => {
|
|
1109
|
+
const paths = doc.paths;
|
|
1110
|
+
if (!isRecord2(paths)) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
for (const pathItem of Object.values(paths)) {
|
|
1114
|
+
if (!isRecord2(pathItem)) {
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
relocateInPathItem(pathItem);
|
|
1118
|
+
}
|
|
1119
|
+
}, "relocateExtensionOperations");
|
|
1120
|
+
var relocateInPathItem = /* @__PURE__ */ chunkEV5I5HGT_js.__name((pathItem) => {
|
|
1121
|
+
const relocated = {};
|
|
1122
|
+
for (const method of EXTENSION_OPERATION_KEYS) {
|
|
1123
|
+
const operation = pathItem[method];
|
|
1124
|
+
if (!isRecord2(operation)) {
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
relocated[method.toUpperCase()] = operation;
|
|
1128
|
+
delete pathItem[method];
|
|
1129
|
+
}
|
|
1130
|
+
if (Object.keys(relocated).length === 0) {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const existing = pathItem[ADDITIONAL_OPERATIONS_KEY];
|
|
1134
|
+
pathItem[ADDITIONAL_OPERATIONS_KEY] = isRecord2(existing) ? {
|
|
1135
|
+
...relocated,
|
|
1136
|
+
...existing
|
|
1137
|
+
} : relocated;
|
|
1138
|
+
}, "relocateInPathItem");
|
|
1044
1139
|
var URI = /* @__PURE__ */ chunkEV5I5HGT_js.__name((id) => `#/components/schemas/${id}`, "URI");
|
|
1045
1140
|
var bulkEmit = /* @__PURE__ */ chunkEV5I5HGT_js.__name((opts) => {
|
|
1046
1141
|
const knownIds = new Set(opts.registry.ids());
|
|
@@ -1080,46 +1175,6 @@ var runPass = /* @__PURE__ */ chunkEV5I5HGT_js.__name((opts, io, knownIds) => {
|
|
|
1080
1175
|
return filtered;
|
|
1081
1176
|
}, "runPass");
|
|
1082
1177
|
|
|
1083
|
-
// src/document/http-methods.ts
|
|
1084
|
-
var HTTP_METHODS = [
|
|
1085
|
-
"get",
|
|
1086
|
-
"put",
|
|
1087
|
-
"post",
|
|
1088
|
-
"delete",
|
|
1089
|
-
"options",
|
|
1090
|
-
"head",
|
|
1091
|
-
"patch",
|
|
1092
|
-
"trace",
|
|
1093
|
-
"query",
|
|
1094
|
-
"search",
|
|
1095
|
-
"propfind",
|
|
1096
|
-
"proppatch",
|
|
1097
|
-
"mkcol",
|
|
1098
|
-
"copy",
|
|
1099
|
-
"move",
|
|
1100
|
-
"lock",
|
|
1101
|
-
"unlock"
|
|
1102
|
-
];
|
|
1103
|
-
var forEachOperation = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc, fn) => {
|
|
1104
|
-
const paths = doc.paths;
|
|
1105
|
-
if (paths === null || typeof paths !== "object") {
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
for (const pathItem of Object.values(paths)) {
|
|
1109
|
-
if (pathItem === null || typeof pathItem !== "object") {
|
|
1110
|
-
continue;
|
|
1111
|
-
}
|
|
1112
|
-
const pathRecord = pathItem;
|
|
1113
|
-
for (const method of HTTP_METHODS) {
|
|
1114
|
-
const op = pathRecord[method];
|
|
1115
|
-
if (op === null || typeof op !== "object") {
|
|
1116
|
-
continue;
|
|
1117
|
-
}
|
|
1118
|
-
fn(op);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
}, "forEachOperation");
|
|
1122
|
-
|
|
1123
1178
|
// src/document/walk-refs.ts
|
|
1124
1179
|
var walkRefs = /* @__PURE__ */ chunkEV5I5HGT_js.__name((node, visit) => {
|
|
1125
1180
|
if (node === null || typeof node !== "object") {
|
|
@@ -1192,22 +1247,12 @@ var collectInputExposedIds = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc, class
|
|
|
1192
1247
|
if (!isPlainRecord2(pathItem)) {
|
|
1193
1248
|
continue;
|
|
1194
1249
|
}
|
|
1195
|
-
for (const operation of
|
|
1250
|
+
for (const operation of operationsOfPathItem(pathItem)) {
|
|
1196
1251
|
collectRefsFromOperation(operation, classToDtoId, knownIds, ids);
|
|
1197
1252
|
}
|
|
1198
1253
|
}
|
|
1199
1254
|
return ids;
|
|
1200
1255
|
}, "collectInputExposedIds");
|
|
1201
|
-
var operationsOf = /* @__PURE__ */ chunkEV5I5HGT_js.__name((pathItem) => {
|
|
1202
|
-
const out = [];
|
|
1203
|
-
for (const method of HTTP_METHODS) {
|
|
1204
|
-
const op = pathItem[method];
|
|
1205
|
-
if (isPlainRecord2(op)) {
|
|
1206
|
-
out.push(op);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
return out;
|
|
1210
|
-
}, "operationsOf");
|
|
1211
1256
|
var collectRefsFromOperation = /* @__PURE__ */ chunkEV5I5HGT_js.__name((operation, classToDtoId, knownIds, ids) => {
|
|
1212
1257
|
if (isPlainRecord2(operation.requestBody)) {
|
|
1213
1258
|
collectRefsFromContent(operation.requestBody.content, classToDtoId, knownIds, ids);
|
|
@@ -1269,7 +1314,7 @@ var collectOutputExposedIds = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc, clas
|
|
|
1269
1314
|
if (!isPlainRecord2(pathItem)) {
|
|
1270
1315
|
continue;
|
|
1271
1316
|
}
|
|
1272
|
-
for (const operation of
|
|
1317
|
+
for (const operation of operationsOfPathItem(pathItem)) {
|
|
1273
1318
|
collectRefsFromResponses(operation.responses, classToDtoId, knownIds, ids);
|
|
1274
1319
|
}
|
|
1275
1320
|
}
|
|
@@ -1728,6 +1773,25 @@ var decorateIfPresent = /* @__PURE__ */ chunkEV5I5HGT_js.__name((schemas, key) =
|
|
|
1728
1773
|
};
|
|
1729
1774
|
}, "decorateIfPresent");
|
|
1730
1775
|
|
|
1776
|
+
// src/document/openapi-version.ts
|
|
1777
|
+
var SUPPORTED_OPENAPI_SERIES = [
|
|
1778
|
+
"3.1",
|
|
1779
|
+
"3.2"
|
|
1780
|
+
];
|
|
1781
|
+
var DEFAULT_OPENAPI_VERSION = "3.1.0";
|
|
1782
|
+
var SUPPORTED_VERSION_PATTERN = /^3\.[12]\.\d+(?:-.+)?$/;
|
|
1783
|
+
var resolveOpenApiVersion = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc) => {
|
|
1784
|
+
const declared = doc.openapi;
|
|
1785
|
+
if (typeof declared !== "string" || declared === "") {
|
|
1786
|
+
return DEFAULT_OPENAPI_VERSION;
|
|
1787
|
+
}
|
|
1788
|
+
if (SUPPORTED_VERSION_PATTERN.test(declared)) {
|
|
1789
|
+
return declared;
|
|
1790
|
+
}
|
|
1791
|
+
console.warn(`[zod-nest] Document declares OpenAPI \`${declared}\`, which zod-nest does not emit; emitting \`${DEFAULT_OPENAPI_VERSION}\` instead. Supported: ${SUPPORTED_OPENAPI_SERIES.map((series) => `${series}.x`).join(", ")}. Set one via \`DocumentBuilder.setOpenAPIVersion()\` to silence this.`);
|
|
1792
|
+
return DEFAULT_OPENAPI_VERSION;
|
|
1793
|
+
}, "resolveOpenApiVersion");
|
|
1794
|
+
|
|
1731
1795
|
// src/document/ref-titles.ts
|
|
1732
1796
|
var applyRefTitles = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc) => {
|
|
1733
1797
|
const schemas = doc.components?.schemas;
|
|
@@ -1797,11 +1861,7 @@ var rewriteRefs = /* @__PURE__ */ chunkEV5I5HGT_js.__name((params) => {
|
|
|
1797
1861
|
}
|
|
1798
1862
|
}, "rewriteRefs");
|
|
1799
1863
|
var rewriteResponseSubtree = /* @__PURE__ */ chunkEV5I5HGT_js.__name((pathItem, divergentOutputIds) => {
|
|
1800
|
-
for (const
|
|
1801
|
-
const op = pathItem[method];
|
|
1802
|
-
if (op === null || typeof op !== "object") {
|
|
1803
|
-
continue;
|
|
1804
|
-
}
|
|
1864
|
+
for (const op of operationsOfPathItem(pathItem)) {
|
|
1805
1865
|
const responses = op.responses;
|
|
1806
1866
|
if (responses === null || typeof responses !== "object") {
|
|
1807
1867
|
continue;
|
|
@@ -1880,7 +1940,6 @@ var withForcedExposure = /* @__PURE__ */ chunkEV5I5HGT_js.__name((collected, reg
|
|
|
1880
1940
|
classToDtoId: collected.classToDtoId
|
|
1881
1941
|
};
|
|
1882
1942
|
}, "withForcedExposure");
|
|
1883
|
-
var OPENAPI_VERSION = "3.1.0";
|
|
1884
1943
|
var applyZodNest = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc, opts = {}) => {
|
|
1885
1944
|
const registry = opts.registry ?? chunkQ3SWJZBB_js.defaultRegistry;
|
|
1886
1945
|
const collected = collectUsage(doc, registry);
|
|
@@ -1921,7 +1980,11 @@ var applyZodNest = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc, opts = {}) => {
|
|
|
1921
1980
|
doc,
|
|
1922
1981
|
collected: extended
|
|
1923
1982
|
});
|
|
1924
|
-
|
|
1983
|
+
const openApiVersion = resolveOpenApiVersion(doc);
|
|
1984
|
+
if (openApiVersion.startsWith("3.2.")) {
|
|
1985
|
+
relocateExtensionOperations(doc);
|
|
1986
|
+
}
|
|
1987
|
+
doc.openapi = openApiVersion;
|
|
1925
1988
|
return doc;
|
|
1926
1989
|
}, "applyZodNest");
|
|
1927
1990
|
|