zod-nest 3.6.0 → 3.7.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 CHANGED
@@ -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 and 3.2** — no `3.0` fallback. Pick with `DocumentBuilder.setOpenAPIVersion('3.2.0')`; 3.2 is what makes `QUERY` and WebDAV routes conformant, and what lets a streamed response say `itemSchema` instead of overstating the whole body. `$ref`s emit to the final location; `cleanupOpenApiDoc` is unnecessary.
45
+ - **OpenAPI 3.1 and 3.2** — no `3.0` fallback. Pick with `DocumentBuilder.setOpenAPIVersion('3.2.0')`; 3.2 is what makes `QUERY` and WebDAV routes conformant, what lets a streamed response say `itemSchema` instead of overstating the whole body, what emits Response Object `summary`, and what collapses a named query DTO to `in: querystring`. `$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`.
@@ -186,7 +186,7 @@ The decorator set: `@ZodBody`, `@ZodQuery`, `@ZodHeaders`, `@ZodCookies`. All ar
186
186
 
187
187
  See [`docs/recipes/intersection-with-union.md`](docs/recipes/intersection-with-union.md) for the full pattern.
188
188
 
189
- Named query objects (both `@Query() dto` and `@ZodQuery`) expand to one parameter per field by default. Pass `applyZodNest(raw, { queryParamStyle: 'ref' })` — or `@ZodQuery(schema, { ref: true })` per handler — to instead emit a single schema-based query parameter that `$ref`s the shared component. Same wire format; see [`docs/swagger-integration.md → Query parameter style`](docs/swagger-integration.md#query-parameter-style).
189
+ Named query objects (both `@Query() dto` and `@ZodQuery`) collapse to a single `in: querystring` parameter under OpenAPI 3.2 — the field 3.2 added for exactly this — and expand to one parameter per field under 3.1. Nothing changes on the wire either way. The deprecated `queryParamStyle` / `@ZodQuery({ ref })` flags override the default; see [`docs/swagger-integration.md → Query parameter style`](docs/swagger-integration.md#query-parameter-style).
190
190
 
191
191
  ### I/O suffix rules
192
192
 
package/dist/index.d.mts CHANGED
@@ -603,16 +603,19 @@ interface ZodQueryOptions {
603
603
  /** Registry to register into. Defaults to `defaultRegistry`. */
604
604
  readonly registry?: ZodNestRegistry;
605
605
  /**
606
- * Override how this query DTO is represented in the OpenAPI doc, taking
607
- * precedence over `applyZodNest`'s `queryParamStyle`:
606
+ * Override how this query DTO is represented, taking precedence over
607
+ * `applyZodNest`'s `queryParamStyle` and the target version:
608
608
  *
609
- * - `true` — one single schema-based query parameter that `$ref`s the DTO's
610
- * `components.schemas` entry (`style: 'form'`, `explode: true`).
609
+ * - `true` — collapse to one parameter carrying the whole schema
610
+ * (`in: 'querystring'` under 3.2, `style: 'form'` under 3.1).
611
611
  * - `false` — one parameter per top-level property.
612
- * - unset — follow the global `queryParamStyle` preference (default `'expand'`).
612
+ * - unset — follow `queryParamStyle`, then the version (3.2 collapses).
613
613
  *
614
- * Ref mode needs a named schema to reference: `ref: true` on an anonymous
614
+ * Collapsing needs a named schema to reference: `ref: true` on an anonymous
615
615
  * schema (no `.meta({ id })` and no `id` option) throws `ZodNestError`.
616
+ *
617
+ * @deprecated Removed in the next major, where 3.1 always expands and 3.2
618
+ * always collapses. Under 3.2 an explicit value warns.
616
619
  */
617
620
  readonly ref?: boolean;
618
621
  }
@@ -731,15 +734,14 @@ declare class ZodNestModule {
731
734
  /**
732
735
  * How a named `@Query()` / `@ZodQuery` DTO is represented in the OpenAPI doc:
733
736
  *
734
- * - `'expand'` (default) — one parameter per top-level property of the DTO
735
- * schema, each inlined or `$ref`'d independently.
736
- * - `'ref'` — a single schema-based query parameter that references the DTO's
737
- * `components.schemas` entry (`style: 'form'`, `explode: true`,
738
- * `schema: { $ref }`). The wire format is identical to `'expand'`; only the
739
- * document representation collapses to the shared component.
737
+ * - `'expand'` — one parameter per top-level property of the DTO schema.
738
+ * - `'ref'` — collapse to a single parameter carrying the whole schema.
739
+ *
740
+ * Query-only: path / header / cookie markers always expand, since collapsing
741
+ * an object into one parameter is a query serialization.
740
742
  *
741
- * Query-only: path / header / cookie markers always expand regardless of this
742
- * setting, since the form-exploded-object pattern is a query serialization.
743
+ * @deprecated Removed in the next major. 3.2 collapses named query DTOs to
744
+ * `in: 'querystring'` by default, which is what this option approximated.
743
745
  */
744
746
  type QueryParamStyle = 'expand' | 'ref';
745
747
 
@@ -759,17 +761,17 @@ interface ApplyZodNestOptions {
759
761
  */
760
762
  strict?: boolean;
761
763
  /**
762
- * How named `@Query()` / `@ZodQuery` DTOs are represented in the document
763
- * (default `'expand'`):
764
+ * Overrides the version-derived default for named `@Query()` / `@ZodQuery`
765
+ * DTOs: `'expand'` for one parameter per property, `'ref'` to collapse to a
766
+ * single parameter carrying the whole schema.
764
767
  *
765
- * - `'expand'` — one query parameter per top-level property of the DTO.
766
- * - `'ref'` — a single schema-based query parameter referencing the DTO's
767
- * `components.schemas` entry (`style: 'form'`, `explode: true`). The wire
768
- * format is unchanged; only the spec representation collapses to the
769
- * shared component. Note: Swagger UI renders the two forms differently.
768
+ * Left unset, 3.2 collapses (as `in: 'querystring'`) and 3.1 expands.
769
+ * Query-only — path / header / cookie DTOs always expand — and a per-handler
770
+ * `@ZodQuery({ ref })` takes precedence.
770
771
  *
771
- * Query-only — path / header / cookie DTOs always expand. A per-handler
772
- * `@ZodQuery({ ref })` override takes precedence over this preference.
772
+ * @deprecated Removed in the next major, where 3.1 always expands and 3.2
773
+ * always collapses. This option existed because 3.1 could not express a
774
+ * whole-query-string schema; 3.2's `querystring` can.
773
775
  */
774
776
  queryParamStyle?: QueryParamStyle;
775
777
  /**
@@ -795,9 +797,9 @@ interface ApplyZodNestOptions {
795
797
  * keyed by the marker's `dtoId` (renaming as needed).
796
798
  * - Every `@Query()` / `@Param()` / `@Headers()` / `@Cookie()` marker
797
799
  * parameter is expanded into one parameter per top-level property of the
798
- * DTO's schema (`expandParamMarkers`) — except `@Query()` DTOs under
799
- * `queryParamStyle: 'ref'` (or a `@ZodQuery({ ref: true })` override), which
800
- * collapse to a single `$ref` schema-based query parameter. The synthetic
800
+ * DTO's schema (`expandParamMarkers`) — except named `@Query()` DTOs under
801
+ * 3.2, which collapse to a single `in: 'querystring'` parameter, and their
802
+ * 3.1 equivalent under `queryParamStyle: 'ref'`. The synthetic
801
803
  * `components.schemas.Object` that `@nestjs/swagger` materialises for the
802
804
  * marker placeholder is pruned when it has no remaining referrers.
803
805
  * - The I/O suffix truth table is applied — equal input/output bodies collapse
@@ -822,6 +824,9 @@ interface ApplyZodNestOptions {
822
824
  * so a streamed body documents one item rather than the whole sequence.
823
825
  * - Response Object `summary` survives only under 3.2; emitting 3.1 drops each
824
826
  * one with a warning naming the operation, since 3.1 forbids the field.
827
+ * - Under 3.2 a named query DTO collapses to `in: 'querystring'`, degrading to
828
+ * per-property expansion (with a warning) where that version's coexistence
829
+ * rules forbid it — a sibling `in: 'query'` parameter, or a second candidate.
825
830
  *
826
831
  * Composable with other doc-transform passes — apply other mutations before
827
832
  * or after this function.
package/dist/index.d.ts CHANGED
@@ -603,16 +603,19 @@ interface ZodQueryOptions {
603
603
  /** Registry to register into. Defaults to `defaultRegistry`. */
604
604
  readonly registry?: ZodNestRegistry;
605
605
  /**
606
- * Override how this query DTO is represented in the OpenAPI doc, taking
607
- * precedence over `applyZodNest`'s `queryParamStyle`:
606
+ * Override how this query DTO is represented, taking precedence over
607
+ * `applyZodNest`'s `queryParamStyle` and the target version:
608
608
  *
609
- * - `true` — one single schema-based query parameter that `$ref`s the DTO's
610
- * `components.schemas` entry (`style: 'form'`, `explode: true`).
609
+ * - `true` — collapse to one parameter carrying the whole schema
610
+ * (`in: 'querystring'` under 3.2, `style: 'form'` under 3.1).
611
611
  * - `false` — one parameter per top-level property.
612
- * - unset — follow the global `queryParamStyle` preference (default `'expand'`).
612
+ * - unset — follow `queryParamStyle`, then the version (3.2 collapses).
613
613
  *
614
- * Ref mode needs a named schema to reference: `ref: true` on an anonymous
614
+ * Collapsing needs a named schema to reference: `ref: true` on an anonymous
615
615
  * schema (no `.meta({ id })` and no `id` option) throws `ZodNestError`.
616
+ *
617
+ * @deprecated Removed in the next major, where 3.1 always expands and 3.2
618
+ * always collapses. Under 3.2 an explicit value warns.
616
619
  */
617
620
  readonly ref?: boolean;
618
621
  }
@@ -731,15 +734,14 @@ declare class ZodNestModule {
731
734
  /**
732
735
  * How a named `@Query()` / `@ZodQuery` DTO is represented in the OpenAPI doc:
733
736
  *
734
- * - `'expand'` (default) — one parameter per top-level property of the DTO
735
- * schema, each inlined or `$ref`'d independently.
736
- * - `'ref'` — a single schema-based query parameter that references the DTO's
737
- * `components.schemas` entry (`style: 'form'`, `explode: true`,
738
- * `schema: { $ref }`). The wire format is identical to `'expand'`; only the
739
- * document representation collapses to the shared component.
737
+ * - `'expand'` — one parameter per top-level property of the DTO schema.
738
+ * - `'ref'` — collapse to a single parameter carrying the whole schema.
739
+ *
740
+ * Query-only: path / header / cookie markers always expand, since collapsing
741
+ * an object into one parameter is a query serialization.
740
742
  *
741
- * Query-only: path / header / cookie markers always expand regardless of this
742
- * setting, since the form-exploded-object pattern is a query serialization.
743
+ * @deprecated Removed in the next major. 3.2 collapses named query DTOs to
744
+ * `in: 'querystring'` by default, which is what this option approximated.
743
745
  */
744
746
  type QueryParamStyle = 'expand' | 'ref';
745
747
 
@@ -759,17 +761,17 @@ interface ApplyZodNestOptions {
759
761
  */
760
762
  strict?: boolean;
761
763
  /**
762
- * How named `@Query()` / `@ZodQuery` DTOs are represented in the document
763
- * (default `'expand'`):
764
+ * Overrides the version-derived default for named `@Query()` / `@ZodQuery`
765
+ * DTOs: `'expand'` for one parameter per property, `'ref'` to collapse to a
766
+ * single parameter carrying the whole schema.
764
767
  *
765
- * - `'expand'` — one query parameter per top-level property of the DTO.
766
- * - `'ref'` — a single schema-based query parameter referencing the DTO's
767
- * `components.schemas` entry (`style: 'form'`, `explode: true`). The wire
768
- * format is unchanged; only the spec representation collapses to the
769
- * shared component. Note: Swagger UI renders the two forms differently.
768
+ * Left unset, 3.2 collapses (as `in: 'querystring'`) and 3.1 expands.
769
+ * Query-only — path / header / cookie DTOs always expand — and a per-handler
770
+ * `@ZodQuery({ ref })` takes precedence.
770
771
  *
771
- * Query-only — path / header / cookie DTOs always expand. A per-handler
772
- * `@ZodQuery({ ref })` override takes precedence over this preference.
772
+ * @deprecated Removed in the next major, where 3.1 always expands and 3.2
773
+ * always collapses. This option existed because 3.1 could not express a
774
+ * whole-query-string schema; 3.2's `querystring` can.
773
775
  */
774
776
  queryParamStyle?: QueryParamStyle;
775
777
  /**
@@ -795,9 +797,9 @@ interface ApplyZodNestOptions {
795
797
  * keyed by the marker's `dtoId` (renaming as needed).
796
798
  * - Every `@Query()` / `@Param()` / `@Headers()` / `@Cookie()` marker
797
799
  * parameter is expanded into one parameter per top-level property of the
798
- * DTO's schema (`expandParamMarkers`) — except `@Query()` DTOs under
799
- * `queryParamStyle: 'ref'` (or a `@ZodQuery({ ref: true })` override), which
800
- * collapse to a single `$ref` schema-based query parameter. The synthetic
800
+ * DTO's schema (`expandParamMarkers`) — except named `@Query()` DTOs under
801
+ * 3.2, which collapse to a single `in: 'querystring'` parameter, and their
802
+ * 3.1 equivalent under `queryParamStyle: 'ref'`. The synthetic
801
803
  * `components.schemas.Object` that `@nestjs/swagger` materialises for the
802
804
  * marker placeholder is pruned when it has no remaining referrers.
803
805
  * - The I/O suffix truth table is applied — equal input/output bodies collapse
@@ -822,6 +824,9 @@ interface ApplyZodNestOptions {
822
824
  * so a streamed body documents one item rather than the whole sequence.
823
825
  * - Response Object `summary` survives only under 3.2; emitting 3.1 drops each
824
826
  * one with a warning naming the operation, since 3.1 forbids the field.
827
+ * - Under 3.2 a named query DTO collapses to `in: 'querystring'`, degrading to
828
+ * per-property expansion (with a warning) where that version's coexistence
829
+ * rules forbid it — a sibling `in: 'query'` parameter, or a second candidate.
825
830
  *
826
831
  * Composable with other doc-transform passes — apply other mutations before
827
832
  * or after this function.
package/dist/index.js CHANGED
@@ -1430,35 +1430,104 @@ var hintFor = /* @__PURE__ */ chunkEV5I5HGT_js.__name((ref, collected) => {
1430
1430
  return "no DTO with this id was registered \u2014 check for a meta.id typo or a DTO used without createZodDto";
1431
1431
  }, "hintFor");
1432
1432
 
1433
- // src/document/expand-param-markers.ts
1433
+ // src/document/querystring-param.ts
1434
+ var QUERYSTRING_MEDIA_TYPE = "application/x-www-form-urlencoded";
1434
1435
  var isPlainRecord3 = /* @__PURE__ */ chunkEV5I5HGT_js.__name((value) => value !== null && typeof value === "object" && !Array.isArray(value), "isPlainRecord");
1436
+ var buildQuerystringParam = /* @__PURE__ */ chunkEV5I5HGT_js.__name((params) => {
1437
+ const { dtoId, body } = params;
1438
+ const required = isPlainRecord3(body) && Array.isArray(body.required) && body.required.length > 0;
1439
+ return {
1440
+ name: dtoId,
1441
+ in: "querystring",
1442
+ required,
1443
+ content: {
1444
+ [QUERYSTRING_MEDIA_TYPE]: {
1445
+ schema: {
1446
+ $ref: `${chunkJ2MGUXBB_js.COMPONENTS_SCHEMAS_PREFIX}${dtoId}`
1447
+ }
1448
+ }
1449
+ }
1450
+ };
1451
+ }, "buildQuerystringParam");
1452
+ var hasQueryConflict = /* @__PURE__ */ chunkEV5I5HGT_js.__name((params) => {
1453
+ const { parameters, pathItemParameters, candidateCount } = params;
1454
+ if (candidateCount > 1) {
1455
+ return true;
1456
+ }
1457
+ return containsQueryParam(parameters) || containsQueryParam(pathItemParameters);
1458
+ }, "hasQueryConflict");
1459
+ var conflictingQueryNames = /* @__PURE__ */ chunkEV5I5HGT_js.__name((parameters, pathItemParameters) => [
1460
+ ...queryNamesOf(parameters),
1461
+ ...queryNamesOf(pathItemParameters)
1462
+ ], "conflictingQueryNames");
1463
+ var containsQueryParam = /* @__PURE__ */ chunkEV5I5HGT_js.__name((parameters) => Array.isArray(parameters) && parameters.some(isPlainQueryParam), "containsQueryParam");
1464
+ var queryNamesOf = /* @__PURE__ */ chunkEV5I5HGT_js.__name((parameters) => {
1465
+ if (!Array.isArray(parameters)) {
1466
+ return [];
1467
+ }
1468
+ const names = [];
1469
+ for (const param of parameters) {
1470
+ if (!isPlainQueryParam(param)) {
1471
+ continue;
1472
+ }
1473
+ names.push(typeof param.name === "string" ? param.name : "<unnamed>");
1474
+ }
1475
+ return names;
1476
+ }, "queryNamesOf");
1477
+ var isPlainQueryParam = /* @__PURE__ */ chunkEV5I5HGT_js.__name((param) => isPlainRecord3(param) && param.in === "query" && param.__zodNestDto !== true, "isPlainQueryParam");
1478
+
1479
+ // src/document/expand-param-markers.ts
1480
+ var isPlainRecord4 = /* @__PURE__ */ chunkEV5I5HGT_js.__name((value) => value !== null && typeof value === "object" && !Array.isArray(value), "isPlainRecord");
1435
1481
  var expandParamMarkers = /* @__PURE__ */ chunkEV5I5HGT_js.__name((params) => {
1436
- const { doc, inputSchemas, outputSchemas } = params;
1437
- const queryParamStyle = params.queryParamStyle ?? "expand";
1482
+ const { doc, inputSchemas, outputSchemas, queryParamStyle } = params;
1483
+ const emitThirtyTwo = params.emitThirtyTwo ?? false;
1438
1484
  const schemas = doc.components?.schemas;
1439
1485
  const componentIds = schemas !== null && typeof schemas === "object" ? new Set(Object.keys(schemas)) : /* @__PURE__ */ new Set();
1486
+ if (queryParamStyle !== void 0 && emitThirtyTwo) {
1487
+ warnDeprecatedQueryParamStyle();
1488
+ }
1489
+ const context = {
1490
+ inputSchemas,
1491
+ outputSchemas,
1492
+ queryParamStyle,
1493
+ emitThirtyTwo,
1494
+ componentIds
1495
+ };
1440
1496
  let expandedAny = false;
1441
- forEachOperation(doc, (op) => {
1442
- const parameters = op.parameters;
1443
- if (!Array.isArray(parameters)) {
1444
- return;
1497
+ for (const [path, pathItem] of Object.entries(doc.paths ?? {})) {
1498
+ if (!isPlainRecord4(pathItem)) {
1499
+ continue;
1445
1500
  }
1446
- const next = expandParameterList(parameters, {
1447
- inputSchemas,
1448
- outputSchemas,
1449
- queryParamStyle,
1450
- componentIds
1451
- });
1452
- if (next !== parameters) {
1453
- op.parameters = next;
1454
- expandedAny = true;
1501
+ for (const { method, operation } of operationEntriesOfPathItem(pathItem)) {
1502
+ const parameters = operation.parameters;
1503
+ if (!Array.isArray(parameters)) {
1504
+ continue;
1505
+ }
1506
+ const next = expandParameterList(parameters, context, {
1507
+ path,
1508
+ method,
1509
+ pathItemParameters: pathItem.parameters
1510
+ });
1511
+ if (next !== parameters) {
1512
+ operation.parameters = next;
1513
+ expandedAny = true;
1514
+ }
1455
1515
  }
1456
- });
1516
+ }
1457
1517
  if (expandedAny) {
1458
1518
  pruneOrphanObjectSchema(doc);
1459
1519
  }
1460
1520
  }, "expandParamMarkers");
1461
- var expandParameterList = /* @__PURE__ */ chunkEV5I5HGT_js.__name((parameters, context) => {
1521
+ var expandParameterList = /* @__PURE__ */ chunkEV5I5HGT_js.__name((parameters, context, scope) => {
1522
+ const candidateCount = countCollapseCandidates(parameters, context);
1523
+ const collapseBlocked = context.emitThirtyTwo && candidateCount > 0 && hasQueryConflict({
1524
+ parameters,
1525
+ pathItemParameters: scope.pathItemParameters,
1526
+ candidateCount
1527
+ });
1528
+ if (collapseBlocked) {
1529
+ warnQuerystringDegraded(scope, parameters, candidateCount);
1530
+ }
1462
1531
  let result;
1463
1532
  for (let i = 0; i < parameters.length; i++) {
1464
1533
  const param = parameters[i];
@@ -1472,21 +1541,51 @@ var expandParameterList = /* @__PURE__ */ chunkEV5I5HGT_js.__name((parameters, c
1472
1541
  }
1473
1542
  const map = marker.io === "output" ? context.outputSchemas : context.inputSchemas;
1474
1543
  const body = map.get(marker.dtoId);
1475
- result.push(...resolveMarker(marker, body, context));
1544
+ result.push(...resolveMarker(marker, body, context, collapseBlocked));
1476
1545
  }
1477
1546
  return result ?? parameters;
1478
1547
  }, "expandParameterList");
1479
- var resolveMarker = /* @__PURE__ */ chunkEV5I5HGT_js.__name((marker, body, context) => {
1480
- const useRef = marker.ref ?? context.queryParamStyle === "ref";
1481
- if (marker.in === "query" && useRef && context.componentIds.has(marker.dtoId)) {
1548
+ var wouldCollapse = /* @__PURE__ */ chunkEV5I5HGT_js.__name((marker, context) => marker.in === "query" && prefersCollapse(marker, context) && context.componentIds.has(marker.dtoId), "wouldCollapse");
1549
+ var prefersCollapse = /* @__PURE__ */ chunkEV5I5HGT_js.__name((marker, context) => {
1550
+ if (marker.ref !== void 0) {
1551
+ return marker.ref;
1552
+ }
1553
+ if (context.queryParamStyle !== void 0) {
1554
+ return context.queryParamStyle === "ref";
1555
+ }
1556
+ return context.emitThirtyTwo;
1557
+ }, "prefersCollapse");
1558
+ var countCollapseCandidates = /* @__PURE__ */ chunkEV5I5HGT_js.__name((parameters, context) => {
1559
+ let count = 0;
1560
+ for (const param of parameters) {
1561
+ const marker = readMarker2(param);
1562
+ if (marker !== void 0 && wouldCollapse(marker, context)) {
1563
+ count += 1;
1564
+ }
1565
+ }
1566
+ return count;
1567
+ }, "countCollapseCandidates");
1568
+ var resolveMarker = /* @__PURE__ */ chunkEV5I5HGT_js.__name((marker, body, context, collapseBlocked) => {
1569
+ if (marker.ref !== void 0 && context.emitThirtyTwo) {
1570
+ warnDeprecatedRefOption(marker.dtoId);
1571
+ }
1572
+ if (collapseBlocked || !wouldCollapse(marker, context)) {
1573
+ return expandOne(marker, body);
1574
+ }
1575
+ if (context.emitThirtyTwo) {
1482
1576
  return [
1483
- buildRefQueryParam(marker, body)
1577
+ buildQuerystringParam({
1578
+ dtoId: marker.dtoId,
1579
+ body
1580
+ })
1484
1581
  ];
1485
1582
  }
1486
- return expandOne(marker, body);
1583
+ return [
1584
+ buildRefQueryParam(marker, body)
1585
+ ];
1487
1586
  }, "resolveMarker");
1488
1587
  var buildRefQueryParam = /* @__PURE__ */ chunkEV5I5HGT_js.__name((marker, body) => {
1489
- const required = isPlainRecord3(body) && Array.isArray(body.required) && body.required.length > 0;
1588
+ const required = isPlainRecord4(body) && Array.isArray(body.required) && body.required.length > 0;
1490
1589
  return {
1491
1590
  name: marker.dtoId,
1492
1591
  in: "query",
@@ -1499,7 +1598,7 @@ var buildRefQueryParam = /* @__PURE__ */ chunkEV5I5HGT_js.__name((marker, body)
1499
1598
  };
1500
1599
  }, "buildRefQueryParam");
1501
1600
  var readMarker2 = /* @__PURE__ */ chunkEV5I5HGT_js.__name((value) => {
1502
- if (!isPlainRecord3(value)) {
1601
+ if (!isPlainRecord4(value)) {
1503
1602
  return void 0;
1504
1603
  }
1505
1604
  if (value.__zodNestDto !== true) {
@@ -1520,7 +1619,7 @@ var readMarker2 = /* @__PURE__ */ chunkEV5I5HGT_js.__name((value) => {
1520
1619
  return value;
1521
1620
  }, "readMarker");
1522
1621
  var expandOne = /* @__PURE__ */ chunkEV5I5HGT_js.__name((marker, body) => {
1523
- if (!isPlainRecord3(body) || !isPlainRecord3(body.properties)) {
1622
+ if (!isPlainRecord4(body) || !isPlainRecord4(body.properties)) {
1524
1623
  throw new ZodNestDocumentError("UNEXPANDABLE_PARAM_DTO", `Cannot expand \`@${capitalize(marker.in)}() x: ${marker.dtoId}\` \u2014 the DTO's schema is not an object with \`properties\`. Non-body parameter DTOs must be object schemas; arrays, unions, primitives, etc. cannot be split into individual parameters. Use \`@Body()\` for non-object DTOs, or restructure the schema as an object whose fields become the params.`, {
1525
1624
  dtoId: marker.dtoId,
1526
1625
  in: marker.in,
@@ -1531,7 +1630,7 @@ var expandOne = /* @__PURE__ */ chunkEV5I5HGT_js.__name((marker, body) => {
1531
1630
  const requiredSet = collectRequired(body.required);
1532
1631
  const out = [];
1533
1632
  for (const [propName, propSchemaRaw] of Object.entries(properties)) {
1534
- if (!isPlainRecord3(propSchemaRaw)) {
1633
+ if (!isPlainRecord4(propSchemaRaw)) {
1535
1634
  continue;
1536
1635
  }
1537
1636
  out.push(buildParameter(marker, propName, propSchemaRaw, requiredSet.has(propName)));
@@ -1564,6 +1663,22 @@ var buildParameter = /* @__PURE__ */ chunkEV5I5HGT_js.__name((marker, name, sche
1564
1663
  };
1565
1664
  }, "buildParameter");
1566
1665
  var capitalize = /* @__PURE__ */ chunkEV5I5HGT_js.__name((value) => value.charAt(0).toUpperCase() + value.slice(1), "capitalize");
1666
+ var warnQuerystringDegraded = /* @__PURE__ */ chunkEV5I5HGT_js.__name((scope, parameters, candidateCount) => {
1667
+ console.warn(`[zod-nest] Expanded the query DTO on \`${scope.method.toUpperCase()} ${scope.path}\` per-property instead of emitting \`in: 'querystring'\`: ${degradeReason(scope, parameters, candidateCount)} The expanded form is spec-valid, so the document still ships.`);
1668
+ }, "warnQuerystringDegraded");
1669
+ var degradeReason = /* @__PURE__ */ chunkEV5I5HGT_js.__name((scope, parameters, candidateCount) => {
1670
+ if (candidateCount > 1) {
1671
+ return `OpenAPI 3.2 allows at most one \`querystring\` parameter per operation, and this one has ${candidateCount} query DTOs.`;
1672
+ }
1673
+ const names = conflictingQueryNames(parameters, scope.pathItemParameters);
1674
+ return `OpenAPI 3.2 forbids a \`querystring\` parameter alongside \`in: 'query'\` parameters (${names.map((name) => `\`${name}\``).join(", ")}).`;
1675
+ }, "degradeReason");
1676
+ var warnDeprecatedQueryParamStyle = /* @__PURE__ */ chunkEV5I5HGT_js.__name(() => {
1677
+ console.warn("[zod-nest] `queryParamStyle` is deprecated and will be removed in the next major: OpenAPI 3.2 emits `in: 'querystring'` for named query DTOs by default, which is what this option approximated. Drop it to take the default.");
1678
+ }, "warnDeprecatedQueryParamStyle");
1679
+ var warnDeprecatedRefOption = /* @__PURE__ */ chunkEV5I5HGT_js.__name((dtoId) => {
1680
+ console.warn(`[zod-nest] \`@ZodQuery({ ref })\` on \`${dtoId}\` is deprecated and will be removed in the next major: OpenAPI 3.2 emits \`in: 'querystring'\` for named query DTOs by default. Drop the option to take the default.`);
1681
+ }, "warnDeprecatedRefOption");
1567
1682
  var pruneOrphanObjectSchema = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc) => {
1568
1683
  const schemas = doc.components?.schemas;
1569
1684
  if (schemas === void 0 || !Object.prototype.hasOwnProperty.call(schemas, "Object")) {
@@ -1635,7 +1750,7 @@ var inlineAnonymousBodies = /* @__PURE__ */ chunkEV5I5HGT_js.__name(({ doc, regi
1635
1750
  `${anonId}${OUTPUT_SUFFIX}`
1636
1751
  ]) {
1637
1752
  const body = schemas[key];
1638
- if (isPlainRecord4(body)) {
1753
+ if (isPlainRecord5(body)) {
1639
1754
  bodyByRef.set(`${chunkJ2MGUXBB_js.COMPONENTS_SCHEMAS_PREFIX}${key}`, body);
1640
1755
  }
1641
1756
  }
@@ -1661,7 +1776,7 @@ var refsWithin = /* @__PURE__ */ chunkEV5I5HGT_js.__name((schemas, bodyByRef) =>
1661
1776
  });
1662
1777
  return found;
1663
1778
  }, "refsWithin");
1664
- var isPlainRecord4 = /* @__PURE__ */ chunkEV5I5HGT_js.__name((value) => value !== null && typeof value === "object" && !Array.isArray(value), "isPlainRecord");
1779
+ var isPlainRecord5 = /* @__PURE__ */ chunkEV5I5HGT_js.__name((value) => value !== null && typeof value === "object" && !Array.isArray(value), "isPlainRecord");
1665
1780
  var inlineRefs = /* @__PURE__ */ chunkEV5I5HGT_js.__name((node, bodyByRef) => {
1666
1781
  if (Array.isArray(node)) {
1667
1782
  for (const item of node) {
@@ -1669,7 +1784,7 @@ var inlineRefs = /* @__PURE__ */ chunkEV5I5HGT_js.__name((node, bodyByRef) => {
1669
1784
  }
1670
1785
  return;
1671
1786
  }
1672
- if (!isPlainRecord4(node)) {
1787
+ if (!isPlainRecord5(node)) {
1673
1788
  return;
1674
1789
  }
1675
1790
  const ref = node.$ref;
@@ -1901,7 +2016,7 @@ var applyRefTitles = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc) => {
1901
2016
  }
1902
2017
  const titleById = /* @__PURE__ */ new Map();
1903
2018
  for (const [id, body] of Object.entries(schemas)) {
1904
- if (isPlainRecord5(body) && typeof body.title === "string" && body.title !== "") {
2019
+ if (isPlainRecord6(body) && typeof body.title === "string" && body.title !== "") {
1905
2020
  titleById.set(id, body.title);
1906
2021
  }
1907
2022
  }
@@ -1911,7 +2026,7 @@ var applyRefTitles = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc) => {
1911
2026
  addRefTitles(doc.paths, titleById);
1912
2027
  addRefTitles(schemas, titleById);
1913
2028
  }, "applyRefTitles");
1914
- var isPlainRecord5 = /* @__PURE__ */ chunkEV5I5HGT_js.__name((value) => value !== null && typeof value === "object" && !Array.isArray(value), "isPlainRecord");
2029
+ var isPlainRecord6 = /* @__PURE__ */ chunkEV5I5HGT_js.__name((value) => value !== null && typeof value === "object" && !Array.isArray(value), "isPlainRecord");
1915
2030
  var addRefTitles = /* @__PURE__ */ chunkEV5I5HGT_js.__name((node, titleById) => {
1916
2031
  if (Array.isArray(node)) {
1917
2032
  for (const item of node) {
@@ -1919,7 +2034,7 @@ var addRefTitles = /* @__PURE__ */ chunkEV5I5HGT_js.__name((node, titleById) =>
1919
2034
  }
1920
2035
  return;
1921
2036
  }
1922
- if (!isPlainRecord5(node)) {
2037
+ if (!isPlainRecord6(node)) {
1923
2038
  return;
1924
2039
  }
1925
2040
  const ref = node.$ref;
@@ -2067,6 +2182,8 @@ var withForcedExposure = /* @__PURE__ */ chunkEV5I5HGT_js.__name((collected, reg
2067
2182
  }, "withForcedExposure");
2068
2183
  var applyZodNest = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc, opts = {}) => {
2069
2184
  const registry = opts.registry ?? chunkJ2MGUXBB_js.defaultRegistry;
2185
+ const openApiVersion = resolveOpenApiVersion(doc);
2186
+ const emitThirtyTwo = openApiVersion.startsWith("3.2.");
2070
2187
  const collected = collectUsage(doc, registry);
2071
2188
  const exposed = withForcedExposure(collected, registry);
2072
2189
  const { inputSchemas, outputSchemas } = bulkEmit({
@@ -2086,7 +2203,8 @@ var applyZodNest = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc, opts = {}) => {
2086
2203
  doc,
2087
2204
  inputSchemas,
2088
2205
  outputSchemas,
2089
- queryParamStyle: opts.queryParamStyle
2206
+ queryParamStyle: opts.queryParamStyle,
2207
+ emitThirtyTwo
2090
2208
  });
2091
2209
  rewriteRefs({
2092
2210
  doc,
@@ -2105,8 +2223,6 @@ var applyZodNest = /* @__PURE__ */ chunkEV5I5HGT_js.__name((doc, opts = {}) => {
2105
2223
  doc,
2106
2224
  collected: extended
2107
2225
  });
2108
- const openApiVersion = resolveOpenApiVersion(doc);
2109
- const emitThirtyTwo = openApiVersion.startsWith("3.2.");
2110
2226
  if (emitThirtyTwo) {
2111
2227
  relocateExtensionOperations(doc);
2112
2228
  }