zod-nest 1.3.0 → 1.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/dist/index.d.mts CHANGED
@@ -446,13 +446,26 @@ interface ZodResponseOptions {
446
446
  passthroughOnError?: boolean;
447
447
  }
448
448
  /**
449
- * Method-only decorator. Declares a typed response variant for the handler.
450
- * Stack multiple decorations to declare per-status types; lookup at runtime
451
- * is by `ZodSerializerInterceptor`'s two-pass matcher (exact numeric, then
452
- * `'NXX'` wildcard).
449
+ * Method-only decorator. Declares a typed response variant for the handler
450
+ * AND applies the equivalent `@ApiResponse(...)` from `@nestjs/swagger` so
451
+ * the OpenAPI document carries the response shape — no need for consumers
452
+ * to hand-write `@ApiResponse` alongside `@ZodResponse`.
453
453
  *
454
- * The wrapped Zod schema (array / tuple) is built once at decoration time
455
- * and stored on the variant record no per-request schema construction.
454
+ * Stack multiple decorations to declare per-status types; runtime lookup
455
+ * is by `ZodSerializerInterceptor`'s two-pass matcher (exact numeric, then
456
+ * `'NXX'` wildcard). The wrapped Zod schema (array / tuple) is built once
457
+ * at decoration time — no per-request schema construction.
458
+ *
459
+ * **Decorator-ordering note (implicit `status` only).** TypeScript decorators
460
+ * apply bottom-up, so `@ZodResponse` (typically written above `@Get` /
461
+ * `@HttpCode`) executes its factory body *first* — before sibling
462
+ * decorators have written their `HTTP_CODE_METADATA` / `METHOD_METADATA`.
463
+ * When `opts.status` is explicit, the `@ApiResponse(...)` call is applied
464
+ * synchronously — there's nothing to wait for. When `opts.status` is
465
+ * implicit (resolves to `@HttpCode` or the HTTP-method default), the
466
+ * `@ApiResponse(...)` call is deferred via `queueMicrotask` so the sibling
467
+ * metadata has settled by the time we read it. See `docs/responses.md →
468
+ * "Decorator ordering & the microtask trick"`.
456
469
  */
457
470
  declare const ZodResponse: (opts: ZodResponseOptions) => MethodDecorator;
458
471
 
package/dist/index.d.ts CHANGED
@@ -446,13 +446,26 @@ interface ZodResponseOptions {
446
446
  passthroughOnError?: boolean;
447
447
  }
448
448
  /**
449
- * Method-only decorator. Declares a typed response variant for the handler.
450
- * Stack multiple decorations to declare per-status types; lookup at runtime
451
- * is by `ZodSerializerInterceptor`'s two-pass matcher (exact numeric, then
452
- * `'NXX'` wildcard).
449
+ * Method-only decorator. Declares a typed response variant for the handler
450
+ * AND applies the equivalent `@ApiResponse(...)` from `@nestjs/swagger` so
451
+ * the OpenAPI document carries the response shape — no need for consumers
452
+ * to hand-write `@ApiResponse` alongside `@ZodResponse`.
453
453
  *
454
- * The wrapped Zod schema (array / tuple) is built once at decoration time
455
- * and stored on the variant record no per-request schema construction.
454
+ * Stack multiple decorations to declare per-status types; runtime lookup
455
+ * is by `ZodSerializerInterceptor`'s two-pass matcher (exact numeric, then
456
+ * `'NXX'` wildcard). The wrapped Zod schema (array / tuple) is built once
457
+ * at decoration time — no per-request schema construction.
458
+ *
459
+ * **Decorator-ordering note (implicit `status` only).** TypeScript decorators
460
+ * apply bottom-up, so `@ZodResponse` (typically written above `@Get` /
461
+ * `@HttpCode`) executes its factory body *first* — before sibling
462
+ * decorators have written their `HTTP_CODE_METADATA` / `METHOD_METADATA`.
463
+ * When `opts.status` is explicit, the `@ApiResponse(...)` call is applied
464
+ * synchronously — there's nothing to wait for. When `opts.status` is
465
+ * implicit (resolves to `@HttpCode` or the HTTP-method default), the
466
+ * `@ApiResponse(...)` call is deferred via `queueMicrotask` so the sibling
467
+ * metadata has settled by the time we read it. See `docs/responses.md →
468
+ * "Decorator ordering & the microtask trick"`.
456
469
  */
457
470
  declare const ZodResponse: (opts: ZodResponseOptions) => MethodDecorator;
458
471
 
package/dist/index.js CHANGED
@@ -2,10 +2,11 @@
2
2
 
3
3
  var zod = require('zod');
4
4
  var common = require('@nestjs/common');
5
+ var constants_js = require('@nestjs/common/constants.js');
6
+ var swagger = require('@nestjs/swagger');
5
7
  var core = require('@nestjs/core');
6
8
  var rxjs = require('rxjs');
7
9
  var operators = require('rxjs/operators');
8
- var constants_js = require('@nestjs/common/constants.js');
9
10
  var stringify = require('fast-json-stable-stringify');
10
11
 
11
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -924,6 +925,25 @@ exports.ZodValidationPipe = _ts_decorate([
924
925
  typeof NormalizedZodNestOptions === "undefined" ? Object : NormalizedZodNestOptions
925
926
  ])
926
927
  ], exports.ZodValidationPipe);
928
+ var POST_DEFAULT_STATUS = 201;
929
+ var GENERIC_DEFAULT_STATUS = 200;
930
+ var defaultStatusFor = /* @__PURE__ */ __name((handler) => {
931
+ const httpCode = Reflect.getMetadata(constants_js.HTTP_CODE_METADATA, handler);
932
+ if (typeof httpCode === "number") {
933
+ return httpCode;
934
+ }
935
+ const method = Reflect.getMetadata(constants_js.METHOD_METADATA, handler);
936
+ if (method === common.RequestMethod.POST) {
937
+ return POST_DEFAULT_STATUS;
938
+ }
939
+ return GENERIC_DEFAULT_STATUS;
940
+ }, "defaultStatusFor");
941
+ var resolveEffectiveStatus = /* @__PURE__ */ __name((variant, handler) => {
942
+ if (variant.status !== void 0) {
943
+ return variant.status;
944
+ }
945
+ return defaultStatusFor(handler);
946
+ }, "resolveEffectiveStatus");
927
947
 
928
948
  // src/response/metadata.ts
929
949
  var ZOD_RESPONSES_METADATA_KEY = /* @__PURE__ */ Symbol.for("zod-nest.responses");
@@ -938,6 +958,67 @@ var appendResponseVariant = /* @__PURE__ */ __name((handler, variant) => {
938
958
  ...existing
939
959
  ], handler);
940
960
  }, "appendResponseVariant");
961
+ var extractDescriptionFields = /* @__PURE__ */ __name((desc) => {
962
+ if (desc === void 0) {
963
+ return {};
964
+ }
965
+ if (typeof desc === "string") {
966
+ return {
967
+ description: desc
968
+ };
969
+ }
970
+ const out = {
971
+ description: desc.description
972
+ };
973
+ if (desc.headers !== void 0) {
974
+ out.headers = desc.headers;
975
+ }
976
+ if (desc.links !== void 0) {
977
+ out.links = desc.links;
978
+ }
979
+ return out;
980
+ }, "extractDescriptionFields");
981
+ var asDtoFunction = /* @__PURE__ */ __name((dto) => dto, "asDtoFunction");
982
+ var buildApiResponseOptions = /* @__PURE__ */ __name((base, body) => {
983
+ return {
984
+ ...base,
985
+ ...body
986
+ };
987
+ }, "buildApiResponseOptions");
988
+ var applySwaggerResponseDecorator = /* @__PURE__ */ __name((variant, effectiveStatus, target, propertyKey, descriptor) => {
989
+ const base = {
990
+ status: effectiveStatus,
991
+ ...extractDescriptionFields(variant.description)
992
+ };
993
+ if (variant.kind === "single") {
994
+ const dto = variant.dto;
995
+ swagger.ApiResponse(buildApiResponseOptions(base, {
996
+ type: asDtoFunction(dto)
997
+ }))(target, propertyKey, descriptor);
998
+ return;
999
+ }
1000
+ if (variant.kind === "array") {
1001
+ const dtos2 = variant.dto;
1002
+ const dto = dtos2[0];
1003
+ swagger.ApiResponse(buildApiResponseOptions(base, {
1004
+ type: asDtoFunction(dto),
1005
+ isArray: true
1006
+ }))(target, propertyKey, descriptor);
1007
+ return;
1008
+ }
1009
+ const dtos = variant.dto;
1010
+ swagger.ApiExtraModels(...dtos.map(asDtoFunction))(target, propertyKey, descriptor);
1011
+ const schema = {
1012
+ type: "array",
1013
+ prefixItems: dtos.map((d) => ({
1014
+ $ref: swagger.getSchemaPath(asDtoFunction(d))
1015
+ })),
1016
+ items: false
1017
+ };
1018
+ swagger.ApiResponse(buildApiResponseOptions(base, {
1019
+ schema
1020
+ }))(target, propertyKey, descriptor);
1021
+ }, "applySwaggerResponseDecorator");
941
1022
 
942
1023
  // src/decorators/zod-response.decorator.ts
943
1024
  var buildArrayKind = /* @__PURE__ */ __name((dtos) => {
@@ -991,7 +1072,8 @@ var normaliseStatus = /* @__PURE__ */ __name((status) => {
991
1072
  var ZodResponse = /* @__PURE__ */ __name((opts) => {
992
1073
  const built = buildKind(opts.type);
993
1074
  const status = normaliseStatus(opts.status);
994
- return (_target, _propertyKey, descriptor) => {
1075
+ const statusExplicit = status !== void 0;
1076
+ return (target, propertyKey, descriptor) => {
995
1077
  const handler = descriptor.value;
996
1078
  if (typeof handler !== "function") {
997
1079
  throw new TypeError("[zod-nest] @ZodResponse can only be applied to methods.");
@@ -1005,29 +1087,17 @@ var ZodResponse = /* @__PURE__ */ __name((opts) => {
1005
1087
  passthroughOnError: opts.passthroughOnError ?? false
1006
1088
  };
1007
1089
  appendResponseVariant(handler, variant);
1090
+ const swaggerDescriptor = descriptor;
1091
+ if (statusExplicit) {
1092
+ applySwaggerResponseDecorator(variant, status, target, propertyKey, swaggerDescriptor);
1093
+ return;
1094
+ }
1095
+ queueMicrotask(() => {
1096
+ const effective = resolveEffectiveStatus(variant, handler);
1097
+ applySwaggerResponseDecorator(variant, effective, target, propertyKey, swaggerDescriptor);
1098
+ });
1008
1099
  };
1009
1100
  }, "ZodResponse");
1010
- var POST_DEFAULT_STATUS = 201;
1011
- var GENERIC_DEFAULT_STATUS = 200;
1012
- var defaultStatusFor = /* @__PURE__ */ __name((handler) => {
1013
- const httpCode = Reflect.getMetadata(constants_js.HTTP_CODE_METADATA, handler);
1014
- if (typeof httpCode === "number") {
1015
- return httpCode;
1016
- }
1017
- const method = Reflect.getMetadata(constants_js.METHOD_METADATA, handler);
1018
- if (method === common.RequestMethod.POST) {
1019
- return POST_DEFAULT_STATUS;
1020
- }
1021
- return GENERIC_DEFAULT_STATUS;
1022
- }, "defaultStatusFor");
1023
- var resolveEffectiveStatus = /* @__PURE__ */ __name((variant, handler) => {
1024
- if (variant.status !== void 0) {
1025
- return variant.status;
1026
- }
1027
- return defaultStatusFor(handler);
1028
- }, "resolveEffectiveStatus");
1029
-
1030
- // src/interceptors/serializer.interceptor.ts
1031
1101
  function _ts_decorate2(decorators, target, key, desc) {
1032
1102
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1033
1103
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1199,6 +1269,25 @@ var HTTP_METHODS = [
1199
1269
  "patch",
1200
1270
  "trace"
1201
1271
  ];
1272
+ var forEachOperation = /* @__PURE__ */ __name((doc, fn) => {
1273
+ const paths = doc.paths;
1274
+ if (paths === null || typeof paths !== "object") {
1275
+ return;
1276
+ }
1277
+ for (const pathItem of Object.values(paths)) {
1278
+ if (pathItem === null || typeof pathItem !== "object") {
1279
+ continue;
1280
+ }
1281
+ const pathRecord = pathItem;
1282
+ for (const method of HTTP_METHODS) {
1283
+ const op = pathRecord[method];
1284
+ if (op === null || typeof op !== "object") {
1285
+ continue;
1286
+ }
1287
+ fn(op);
1288
+ }
1289
+ }
1290
+ }, "forEachOperation");
1202
1291
 
1203
1292
  // src/document/collect-usage.ts
1204
1293
  var isPlainRecord2 = /* @__PURE__ */ __name((value) => value !== null && typeof value === "object" && !Array.isArray(value), "isPlainRecord");
@@ -1441,41 +1530,39 @@ var hintFor = /* @__PURE__ */ __name((ref, collected) => {
1441
1530
  var isPlainRecord3 = /* @__PURE__ */ __name((value) => value !== null && typeof value === "object" && !Array.isArray(value), "isPlainRecord");
1442
1531
  var expandParamMarkers = /* @__PURE__ */ __name((params) => {
1443
1532
  const { doc, inputSchemas, outputSchemas } = params;
1444
- const paths = doc.paths;
1445
- if (!isPlainRecord3(paths)) {
1446
- return;
1447
- }
1448
- for (const pathItem of Object.values(paths)) {
1449
- if (!isPlainRecord3(pathItem)) {
1450
- continue;
1533
+ let expandedAny = false;
1534
+ forEachOperation(doc, (op) => {
1535
+ const parameters = op.parameters;
1536
+ if (!Array.isArray(parameters)) {
1537
+ return;
1451
1538
  }
1452
- for (const method of HTTP_METHODS) {
1453
- const op = pathItem[method];
1454
- if (!isPlainRecord3(op)) {
1455
- continue;
1456
- }
1457
- const parameters = op.parameters;
1458
- if (!Array.isArray(parameters)) {
1459
- continue;
1460
- }
1461
- op.parameters = expandParameterList(parameters, inputSchemas, outputSchemas);
1539
+ const next = expandParameterList(parameters, inputSchemas, outputSchemas);
1540
+ if (next !== parameters) {
1541
+ op.parameters = next;
1542
+ expandedAny = true;
1462
1543
  }
1544
+ });
1545
+ if (expandedAny) {
1546
+ pruneOrphanObjectSchema(doc);
1463
1547
  }
1464
- pruneOrphanObjectSchema(doc);
1465
1548
  }, "expandParamMarkers");
1466
1549
  var expandParameterList = /* @__PURE__ */ __name((parameters, inputSchemas, outputSchemas) => {
1467
- const result = [];
1468
- for (const param of parameters) {
1550
+ let result;
1551
+ for (let i = 0; i < parameters.length; i++) {
1552
+ const param = parameters[i];
1469
1553
  const marker = readMarker2(param);
1470
1554
  if (marker === void 0) {
1471
- result.push(param);
1555
+ result?.push(param);
1472
1556
  continue;
1473
1557
  }
1558
+ if (result === void 0) {
1559
+ result = parameters.slice(0, i);
1560
+ }
1474
1561
  const map = marker.io === "output" ? outputSchemas : inputSchemas;
1475
1562
  const body = map.get(marker.dtoId);
1476
1563
  result.push(...expandOne(marker, body));
1477
1564
  }
1478
- return result;
1565
+ return result ?? parameters;
1479
1566
  }, "expandParameterList");
1480
1567
  var readMarker2 = /* @__PURE__ */ __name((value) => {
1481
1568
  if (!isPlainRecord3(value)) {
@@ -1532,16 +1619,12 @@ var buildParameter = /* @__PURE__ */ __name((marker, name, schema, required) =>
1532
1619
  console.warn(`[zod-nest] Path parameter \`${name}\` on DTO \`${marker.dtoId}\` is marked optional in the Zod schema; OpenAPI 3.1 requires path parameters to be required. Coercing \`required: true\` so the emitted document is spec-valid. Fix by removing \`.optional()\` / \`.nullish()\` from the field, or by switching the decorator to @Query() / @Headers() if the field is genuinely optional.`);
1533
1620
  effectiveRequired = true;
1534
1621
  }
1535
- const entry = {
1622
+ return {
1536
1623
  name,
1537
1624
  in: marker.in,
1538
1625
  required: effectiveRequired,
1539
1626
  schema
1540
1627
  };
1541
- if (typeof schema.description === "string") {
1542
- entry.description = schema.description;
1543
- }
1544
- return entry;
1545
1628
  }, "buildParameter");
1546
1629
  var capitalize = /* @__PURE__ */ __name((value) => value.charAt(0).toUpperCase() + value.slice(1), "capitalize");
1547
1630
  var pruneOrphanObjectSchema = /* @__PURE__ */ __name((doc) => {
@@ -1755,40 +1838,28 @@ var stripMarkers = /* @__PURE__ */ __name((doc) => {
1755
1838
  if (schemas !== void 0) {
1756
1839
  for (const schema of Object.values(schemas)) {
1757
1840
  stripMarkerFromSchema(schema);
1841
+ dropJsonSchemaMetadata(schema);
1758
1842
  }
1759
1843
  }
1760
1844
  stripMarkerParameters(doc);
1761
1845
  }, "stripMarkers");
1762
- var stripMarkerParameters = /* @__PURE__ */ __name((doc) => {
1763
- const paths = doc.paths;
1764
- if (paths === null || typeof paths !== "object") {
1846
+ var dropJsonSchemaMetadata = /* @__PURE__ */ __name((schema) => {
1847
+ if (schema === null || typeof schema !== "object") {
1765
1848
  return;
1766
1849
  }
1767
- for (const pathItem of Object.values(paths)) {
1768
- if (pathItem === null || typeof pathItem !== "object") {
1769
- continue;
1770
- }
1771
- const pathRecord = pathItem;
1772
- for (const method of HTTP_METHODS) {
1773
- const op = pathRecord[method];
1774
- if (op === null || typeof op !== "object") {
1775
- continue;
1776
- }
1777
- const opRecord = op;
1778
- const parameters = opRecord.parameters;
1779
- if (!Array.isArray(parameters)) {
1780
- continue;
1781
- }
1782
- opRecord.parameters = parameters.filter((param) => !isMarkerParam(param));
1850
+ const body = schema;
1851
+ delete body.$schema;
1852
+ delete body.$id;
1853
+ }, "dropJsonSchemaMetadata");
1854
+ var stripMarkerParameters = /* @__PURE__ */ __name((doc) => {
1855
+ forEachOperation(doc, (op) => {
1856
+ const parameters = op.parameters;
1857
+ if (!Array.isArray(parameters)) {
1858
+ return;
1783
1859
  }
1784
- }
1860
+ op.parameters = parameters.filter((param) => !isZodDtoMarker(param));
1861
+ });
1785
1862
  }, "stripMarkerParameters");
1786
- var isMarkerParam = /* @__PURE__ */ __name((value) => {
1787
- if (value === null || typeof value !== "object") {
1788
- return false;
1789
- }
1790
- return value.__zodNestDto === true;
1791
- }, "isMarkerParam");
1792
1863
  var stripMarkerFromSchema = /* @__PURE__ */ __name((schema) => {
1793
1864
  if (schema === null || typeof schema !== "object") {
1794
1865
  return;