zod 4.4.0 → 4.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zod",
3
- "version": "4.4.0",
3
+ "version": "4.4.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -16,7 +16,7 @@ type TestFormErrors = z.inferFlattenedErrors<typeof Test>;
16
16
  test("default flattened errors type inference", () => {
17
17
  type TestTypeErrors = {
18
18
  formErrors: string[];
19
- fieldErrors: { [P in keyof z.TypeOf<typeof Test>]?: string[] | undefined };
19
+ fieldErrors: { [P in keyof z.TypeOf<typeof Test>]?: string[] };
20
20
  };
21
21
 
22
22
  util.assertEqual<z.inferFlattenedErrors<typeof Test>, TestTypeErrors>(true);
@@ -28,7 +28,7 @@ test("custom flattened errors type inference", () => {
28
28
  type TestTypeErrors = {
29
29
  formErrors: ErrorType[];
30
30
  fieldErrors: {
31
- [P in keyof z.TypeOf<typeof Test>]?: ErrorType[] | undefined;
31
+ [P in keyof z.TypeOf<typeof Test>]?: ErrorType[];
32
32
  };
33
33
  };
34
34
 
@@ -40,7 +40,7 @@ test("custom flattened errors type inference", () => {
40
40
  test("form errors type inference", () => {
41
41
  type TestTypeErrors = {
42
42
  formErrors: string[];
43
- fieldErrors: { [P in keyof z.TypeOf<typeof Test>]?: string[] | undefined };
43
+ fieldErrors: { [P in keyof z.TypeOf<typeof Test>]?: string[] };
44
44
  };
45
45
 
46
46
  util.assertEqual<z.inferFlattenedErrors<typeof Test>, TestTypeErrors>(true);
@@ -212,9 +212,9 @@ test("inferred merged object type with optional properties", async () => {
212
212
  .object({ a: z.string(), b: z.string().optional() })
213
213
  .merge(z.object({ a: z.string().optional(), b: z.string() }));
214
214
  type Merged = z.infer<typeof Merged>;
215
- util.assertEqual<Merged, { a?: string; b: string }>(true);
215
+ util.assertEqual<Merged, { a?: string | undefined; b: string }>(true);
216
216
  // todo
217
- // util.assertEqual<Merged, { a?: string; b: string }>(true);
217
+ // util.assertEqual<Merged, { a?: string | undefined; b: string }>(true);
218
218
  });
219
219
 
220
220
  test("inferred unioned object type with optional properties", async () => {
@@ -223,7 +223,7 @@ test("inferred unioned object type with optional properties", async () => {
223
223
  z.object({ a: z.string().optional(), b: z.string() }),
224
224
  ]);
225
225
  type Unioned = z.infer<typeof Unioned>;
226
- util.assertEqual<Unioned, { a: string; b?: string } | { a?: string; b: string }>(true);
226
+ util.assertEqual<Unioned, { a: string; b?: string | undefined } | { a?: string | undefined; b: string }>(true);
227
227
  });
228
228
 
229
229
  test("inferred enum type", async () => {
@@ -245,13 +245,13 @@ test("inferred enum type", async () => {
245
245
  test("inferred partial object type with optional properties", async () => {
246
246
  const Partial = z.object({ a: z.string(), b: z.string().optional() }).partial();
247
247
  type Partial = z.infer<typeof Partial>;
248
- util.assertEqual<Partial, { a?: string; b?: string }>(true);
248
+ util.assertEqual<Partial, { a?: string | undefined; b?: string | undefined }>(true);
249
249
  });
250
250
 
251
251
  test("inferred picked object type with optional properties", async () => {
252
252
  const Picked = z.object({ a: z.string(), b: z.string().optional() }).pick({ b: true });
253
253
  type Picked = z.infer<typeof Picked>;
254
- util.assertEqual<Picked, { b?: string }>(true);
254
+ util.assertEqual<Picked, { b?: string | undefined }>(true);
255
255
  });
256
256
 
257
257
  test("inferred type for unknown/any keys", () => {
@@ -21,7 +21,7 @@ test("shallow inference", () => {
21
21
  name?: string | undefined;
22
22
  age?: number | undefined;
23
23
  outer?: { inner: string } | undefined;
24
- array?: { asdf: string }[];
24
+ array?: { asdf: string }[] | undefined;
25
25
  };
26
26
  util.assertEqual<shallow, correct>(true);
27
27
  });
@@ -41,7 +41,7 @@ test("deep partial inference", () => {
41
41
  asdf.parse("asdf");
42
42
  type deep = z.infer<typeof deep>;
43
43
  type correct = {
44
- array?: { asdf?: string }[];
44
+ array?: { asdf?: string | undefined }[] | undefined;
45
45
  name?: string | undefined;
46
46
  age?: number | undefined;
47
47
  outer?: { inner?: string | undefined } | undefined;
@@ -118,7 +118,7 @@ test("deep partial inference", () => {
118
118
  asdf?: string | undefined;
119
119
  }[]
120
120
  | undefined;
121
- tuple?: [{ value?: string }] | undefined;
121
+ tuple?: [{ value?: string | undefined }] | undefined;
122
122
  };
123
123
  util.assertEqual<expected, partialed>(true);
124
124
  });
@@ -282,11 +282,10 @@ test("tuple result is dense when optional precedes a default", () => {
282
282
  expect(1 in out && 3 in out).toEqual(true);
283
283
  });
284
284
 
285
- test("tuple breaks and truncates on first absent-optional rejection", () => {
286
- // An `.optional()` slot that rejects `undefined` (e.g. via a refine) past
287
- // optStart must (a) swallow the issue, (b) truncate the result there, and
288
- // (c) NOT materialize any later defaults otherwise the parser would
289
- // happily fill in slots after a slot it just decided was missing/invalid.
285
+ test("tuple truncates absent optional rejections only when the output tail is optional", () => {
286
+ // An absent optional-output slot can only be swallowed when every later
287
+ // output slot is optional too. If a later default would make the output tail
288
+ // required, truncating would violate the tuple's output type.
290
289
  const refusesUndefined = z
291
290
  .string()
292
291
  .optional()
@@ -294,13 +293,15 @@ test("tuple breaks and truncates on first absent-optional rejection", () => {
294
293
 
295
294
  const trailingDefault = z.tuple([z.string(), refusesUndefined, z.string().default("d")]);
296
295
  const r1 = trailingDefault.safeParse(["alpha"]);
297
- expect(r1.success).toBe(true);
298
- expect(r1.data).toEqual(["alpha"]);
296
+ expect(r1.success).toBe(false);
297
+ expect(r1.error!.issues[0].path).toEqual([1]);
299
298
 
300
- // Optional slots BEFORE the rejected one collapse away with the truncate
301
- // (mirrors the trailing-trim behaviour for absent optionals).
299
+ // Optional slots BEFORE the rejected one still cannot hide a later required
300
+ // output slot.
302
301
  const beforeReject = z.tuple([z.string(), z.string().optional(), refusesUndefined, z.string().default("d")]);
303
- expect(beforeReject.safeParse(["alpha"]).data).toEqual(["alpha"]);
302
+ const r2 = beforeReject.safeParse(["alpha"]);
303
+ expect(r2.success).toBe(false);
304
+ expect(r2.error!.issues[0].path).toEqual([2]);
304
305
 
305
306
  // No default after — truncate still applies, no spurious issue surfaces.
306
307
  const noTrailingDefault = z.tuple([z.string(), refusesUndefined]);
@@ -309,7 +310,7 @@ test("tuple breaks and truncates on first absent-optional rejection", () => {
309
310
  expect(r3.data).toEqual(["alpha"]);
310
311
  });
311
312
 
312
- test("tuple breaks on absent-optional rejection under async parse", async () => {
313
+ test("tuple rejects absent optional before required output under async parse", async () => {
313
314
  const refusesUndefined = z
314
315
  .string()
315
316
  .optional()
@@ -317,8 +318,24 @@ test("tuple breaks on absent-optional rejection under async parse", async () =>
317
318
 
318
319
  const schema = z.tuple([z.string(), refusesUndefined, z.string().default("d")]);
319
320
  const r = await schema.safeParseAsync(["alpha"]);
320
- expect(r.success).toBe(true);
321
- expect(r.data).toEqual(["alpha"]);
321
+ expect(r.success).toBe(false);
322
+ expect(r.error!.issues[0].path).toEqual([1]);
323
+ });
324
+
325
+ test("tuple rejects absent exact optional before defaulted output", () => {
326
+ const schema = z.tuple([z.string(), z.string().exactOptional(), z.string().default("fallback")]);
327
+ expectTypeOf<typeof schema._output>().toEqualTypeOf<[string, string, string]>();
328
+
329
+ const missingExact = schema.safeParse(["alpha"]);
330
+ expect(missingExact.success).toBe(false);
331
+ expect(missingExact.error!.issues[0].path).toEqual([1]);
332
+
333
+ expect(schema.parse(["alpha", "bravo"])).toEqual(["alpha", "bravo", "fallback"]);
334
+ expect(schema.safeParse(["alpha", undefined]).success).toBe(false);
335
+
336
+ // With no later required output slot, exact optional still behaves like an
337
+ // omitted tuple tail and truncates cleanly.
338
+ expect(z.tuple([z.string(), z.string().exactOptional(), z.string().optional()]).parse(["alpha"])).toEqual(["alpha"]);
322
339
  });
323
340
 
324
341
  test("tuple preserves explicit undefined inside input even for optional-out schemas", () => {
@@ -2677,14 +2677,14 @@ export const $ZodTuple: core.$constructor<$ZodTuple> = /*@__PURE__*/ core.$const
2677
2677
  payload.value = [];
2678
2678
  const proms: Promise<any>[] = [];
2679
2679
 
2680
- const reversedIndex = [...items].reverse().findIndex((item) => item._zod.optin !== "optional");
2681
- const optStart = reversedIndex === -1 ? 0 : items.length - reversedIndex;
2680
+ const optinStart = getTupleOptStart(items, "optin");
2681
+ const optoutStart = getTupleOptStart(items, "optout");
2682
2682
 
2683
2683
  if (!def.rest) {
2684
- if (input.length < optStart) {
2684
+ if (input.length < optinStart) {
2685
2685
  payload.issues.push({
2686
2686
  code: "too_small",
2687
- minimum: optStart,
2687
+ minimum: optinStart,
2688
2688
  inclusive: true,
2689
2689
  input,
2690
2690
  inst,
@@ -2706,9 +2706,8 @@ export const $ZodTuple: core.$constructor<$ZodTuple> = /*@__PURE__*/ core.$const
2706
2706
 
2707
2707
  // Run every item in parallel, collecting results into an indexed
2708
2708
  // array. The post-processing in `handleTupleResults` walks them in
2709
- // order so it can break on the first absent-optional error: once a
2710
- // slot rejects `undefined`, the tuple is malformed at that index and
2711
- // any later defaults must NOT fire.
2709
+ // order so it can decide whether an absent optional-output error can
2710
+ // truncate the tail or must be reported to preserve required output.
2712
2711
  const itemResults: ParsePayload[] = new Array(items.length);
2713
2712
  for (let i = 0; i < items.length; i++) {
2714
2713
  const r = items[i]._zod.run({ value: input[i], issues: [] }, ctx);
@@ -2737,11 +2736,20 @@ export const $ZodTuple: core.$constructor<$ZodTuple> = /*@__PURE__*/ core.$const
2737
2736
  }
2738
2737
  }
2739
2738
 
2740
- if (proms.length) return Promise.all(proms).then(() => handleTupleResults(itemResults, payload, items, input));
2741
- return handleTupleResults(itemResults, payload, items, input);
2739
+ if (proms.length) {
2740
+ return Promise.all(proms).then(() => handleTupleResults(itemResults, payload, items, input, optoutStart));
2741
+ }
2742
+ return handleTupleResults(itemResults, payload, items, input, optoutStart);
2742
2743
  };
2743
2744
  });
2744
2745
 
2746
+ function getTupleOptStart(items: readonly $ZodType[], key: "optin" | "optout") {
2747
+ for (let i = items.length - 1; i >= 0; i--) {
2748
+ if (items[i]._zod[key] !== "optional") return i + 1;
2749
+ }
2750
+ return 0;
2751
+ }
2752
+
2745
2753
  function handleTupleResult(result: ParsePayload, final: ParsePayload<any[]>, index: number) {
2746
2754
  if (result.issues.length) {
2747
2755
  final.issues.push(...util.prefixIssues(index, result.issues));
@@ -2749,30 +2757,21 @@ function handleTupleResult(result: ParsePayload, final: ParsePayload<any[]>, ind
2749
2757
  final.value[index] = result.value;
2750
2758
  }
2751
2759
 
2752
- // Post-processes the per-item results collected by the tuple parser.
2753
- // `optStart` is intentionally NOT consulted here — it's an input-length
2754
- // concern handled by the `too_small` precheck at the top of parse. This
2755
- // step is purely about output shaping, which is governed by `optout`:
2756
- // a `.default()` tail item sits inside the optStart region (its `optin`
2757
- // is optional), but it must NOT be dropped or have its errors swallowed
2758
- // because it materializes a defined value (`optout !== "optional"`).
2759
2760
  function handleTupleResults(
2760
2761
  itemResults: ParsePayload[],
2761
2762
  final: ParsePayload<any[]>,
2762
2763
  items: readonly $ZodType[],
2763
- input: unknown[]
2764
+ input: unknown[],
2765
+ optoutStart: number
2764
2766
  ) {
2765
2767
  // Walk results in order. Mirror $ZodObject's swallow-on-absent-optional
2766
- // rule, but for a tuple "absent" is a positional concept: once we
2767
- // swallow at index k, every later index is also absent-or-corrupted,
2768
- // so we truncate the result there and stop processing — including
2769
- // skipping any later defaults.
2768
+ // rule, but only after `optoutStart`: the first index where the output
2769
+ // tuple tail can be absent.
2770
2770
  for (let i = 0; i < items.length; i++) {
2771
2771
  const r = itemResults[i];
2772
- const isOptionalOut = items[i]._zod.optout === "optional";
2773
2772
  const isPresent = i < input.length;
2774
2773
  if (r.issues.length) {
2775
- if (isOptionalOut && !isPresent) {
2774
+ if (!isPresent && i >= optoutStart) {
2776
2775
  final.value.length = i;
2777
2776
  break;
2778
2777
  }
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 4,
4
- patch: 0 as number,
4
+ patch: 1 as number,
5
5
  } as const;
@@ -1344,13 +1344,13 @@ exports.$ZodTuple = core.$constructor("$ZodTuple", (inst, def) => {
1344
1344
  }
1345
1345
  payload.value = [];
1346
1346
  const proms = [];
1347
- const reversedIndex = [...items].reverse().findIndex((item) => item._zod.optin !== "optional");
1348
- const optStart = reversedIndex === -1 ? 0 : items.length - reversedIndex;
1347
+ const optinStart = getTupleOptStart(items, "optin");
1348
+ const optoutStart = getTupleOptStart(items, "optout");
1349
1349
  if (!def.rest) {
1350
- if (input.length < optStart) {
1350
+ if (input.length < optinStart) {
1351
1351
  payload.issues.push({
1352
1352
  code: "too_small",
1353
- minimum: optStart,
1353
+ minimum: optinStart,
1354
1354
  inclusive: true,
1355
1355
  input,
1356
1356
  inst,
@@ -1371,9 +1371,8 @@ exports.$ZodTuple = core.$constructor("$ZodTuple", (inst, def) => {
1371
1371
  }
1372
1372
  // Run every item in parallel, collecting results into an indexed
1373
1373
  // array. The post-processing in `handleTupleResults` walks them in
1374
- // order so it can break on the first absent-optional error: once a
1375
- // slot rejects `undefined`, the tuple is malformed at that index and
1376
- // any later defaults must NOT fire.
1374
+ // order so it can decide whether an absent optional-output error can
1375
+ // truncate the tail or must be reported to preserve required output.
1377
1376
  const itemResults = new Array(items.length);
1378
1377
  for (let i = 0; i < items.length; i++) {
1379
1378
  const r = items[i]._zod.run({ value: input[i], issues: [] }, ctx);
@@ -1400,36 +1399,34 @@ exports.$ZodTuple = core.$constructor("$ZodTuple", (inst, def) => {
1400
1399
  }
1401
1400
  }
1402
1401
  }
1403
- if (proms.length)
1404
- return Promise.all(proms).then(() => handleTupleResults(itemResults, payload, items, input));
1405
- return handleTupleResults(itemResults, payload, items, input);
1402
+ if (proms.length) {
1403
+ return Promise.all(proms).then(() => handleTupleResults(itemResults, payload, items, input, optoutStart));
1404
+ }
1405
+ return handleTupleResults(itemResults, payload, items, input, optoutStart);
1406
1406
  };
1407
1407
  });
1408
+ function getTupleOptStart(items, key) {
1409
+ for (let i = items.length - 1; i >= 0; i--) {
1410
+ if (items[i]._zod[key] !== "optional")
1411
+ return i + 1;
1412
+ }
1413
+ return 0;
1414
+ }
1408
1415
  function handleTupleResult(result, final, index) {
1409
1416
  if (result.issues.length) {
1410
1417
  final.issues.push(...util.prefixIssues(index, result.issues));
1411
1418
  }
1412
1419
  final.value[index] = result.value;
1413
1420
  }
1414
- // Post-processes the per-item results collected by the tuple parser.
1415
- // `optStart` is intentionally NOT consulted here — it's an input-length
1416
- // concern handled by the `too_small` precheck at the top of parse. This
1417
- // step is purely about output shaping, which is governed by `optout`:
1418
- // a `.default()` tail item sits inside the optStart region (its `optin`
1419
- // is optional), but it must NOT be dropped or have its errors swallowed
1420
- // because it materializes a defined value (`optout !== "optional"`).
1421
- function handleTupleResults(itemResults, final, items, input) {
1421
+ function handleTupleResults(itemResults, final, items, input, optoutStart) {
1422
1422
  // Walk results in order. Mirror $ZodObject's swallow-on-absent-optional
1423
- // rule, but for a tuple "absent" is a positional concept: once we
1424
- // swallow at index k, every later index is also absent-or-corrupted,
1425
- // so we truncate the result there and stop processing — including
1426
- // skipping any later defaults.
1423
+ // rule, but only after `optoutStart`: the first index where the output
1424
+ // tuple tail can be absent.
1427
1425
  for (let i = 0; i < items.length; i++) {
1428
1426
  const r = itemResults[i];
1429
- const isOptionalOut = items[i]._zod.optout === "optional";
1430
1427
  const isPresent = i < input.length;
1431
1428
  if (r.issues.length) {
1432
- if (isOptionalOut && !isPresent) {
1429
+ if (!isPresent && i >= optoutStart) {
1433
1430
  final.value.length = i;
1434
1431
  break;
1435
1432
  }
@@ -1313,13 +1313,13 @@ export const $ZodTuple = /*@__PURE__*/ core.$constructor("$ZodTuple", (inst, def
1313
1313
  }
1314
1314
  payload.value = [];
1315
1315
  const proms = [];
1316
- const reversedIndex = [...items].reverse().findIndex((item) => item._zod.optin !== "optional");
1317
- const optStart = reversedIndex === -1 ? 0 : items.length - reversedIndex;
1316
+ const optinStart = getTupleOptStart(items, "optin");
1317
+ const optoutStart = getTupleOptStart(items, "optout");
1318
1318
  if (!def.rest) {
1319
- if (input.length < optStart) {
1319
+ if (input.length < optinStart) {
1320
1320
  payload.issues.push({
1321
1321
  code: "too_small",
1322
- minimum: optStart,
1322
+ minimum: optinStart,
1323
1323
  inclusive: true,
1324
1324
  input,
1325
1325
  inst,
@@ -1340,9 +1340,8 @@ export const $ZodTuple = /*@__PURE__*/ core.$constructor("$ZodTuple", (inst, def
1340
1340
  }
1341
1341
  // Run every item in parallel, collecting results into an indexed
1342
1342
  // array. The post-processing in `handleTupleResults` walks them in
1343
- // order so it can break on the first absent-optional error: once a
1344
- // slot rejects `undefined`, the tuple is malformed at that index and
1345
- // any later defaults must NOT fire.
1343
+ // order so it can decide whether an absent optional-output error can
1344
+ // truncate the tail or must be reported to preserve required output.
1346
1345
  const itemResults = new Array(items.length);
1347
1346
  for (let i = 0; i < items.length; i++) {
1348
1347
  const r = items[i]._zod.run({ value: input[i], issues: [] }, ctx);
@@ -1369,36 +1368,34 @@ export const $ZodTuple = /*@__PURE__*/ core.$constructor("$ZodTuple", (inst, def
1369
1368
  }
1370
1369
  }
1371
1370
  }
1372
- if (proms.length)
1373
- return Promise.all(proms).then(() => handleTupleResults(itemResults, payload, items, input));
1374
- return handleTupleResults(itemResults, payload, items, input);
1371
+ if (proms.length) {
1372
+ return Promise.all(proms).then(() => handleTupleResults(itemResults, payload, items, input, optoutStart));
1373
+ }
1374
+ return handleTupleResults(itemResults, payload, items, input, optoutStart);
1375
1375
  };
1376
1376
  });
1377
+ function getTupleOptStart(items, key) {
1378
+ for (let i = items.length - 1; i >= 0; i--) {
1379
+ if (items[i]._zod[key] !== "optional")
1380
+ return i + 1;
1381
+ }
1382
+ return 0;
1383
+ }
1377
1384
  function handleTupleResult(result, final, index) {
1378
1385
  if (result.issues.length) {
1379
1386
  final.issues.push(...util.prefixIssues(index, result.issues));
1380
1387
  }
1381
1388
  final.value[index] = result.value;
1382
1389
  }
1383
- // Post-processes the per-item results collected by the tuple parser.
1384
- // `optStart` is intentionally NOT consulted here — it's an input-length
1385
- // concern handled by the `too_small` precheck at the top of parse. This
1386
- // step is purely about output shaping, which is governed by `optout`:
1387
- // a `.default()` tail item sits inside the optStart region (its `optin`
1388
- // is optional), but it must NOT be dropped or have its errors swallowed
1389
- // because it materializes a defined value (`optout !== "optional"`).
1390
- function handleTupleResults(itemResults, final, items, input) {
1390
+ function handleTupleResults(itemResults, final, items, input, optoutStart) {
1391
1391
  // Walk results in order. Mirror $ZodObject's swallow-on-absent-optional
1392
- // rule, but for a tuple "absent" is a positional concept: once we
1393
- // swallow at index k, every later index is also absent-or-corrupted,
1394
- // so we truncate the result there and stop processing — including
1395
- // skipping any later defaults.
1392
+ // rule, but only after `optoutStart`: the first index where the output
1393
+ // tuple tail can be absent.
1396
1394
  for (let i = 0; i < items.length; i++) {
1397
1395
  const r = itemResults[i];
1398
- const isOptionalOut = items[i]._zod.optout === "optional";
1399
1396
  const isPresent = i < input.length;
1400
1397
  if (r.issues.length) {
1401
- if (isOptionalOut && !isPresent) {
1398
+ if (!isPresent && i >= optoutStart) {
1402
1399
  final.value.length = i;
1403
1400
  break;
1404
1401
  }
@@ -4,5 +4,5 @@ exports.version = void 0;
4
4
  exports.version = {
5
5
  major: 4,
6
6
  minor: 4,
7
- patch: 0,
7
+ patch: 1,
8
8
  };
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 4,
4
- patch: 0,
4
+ patch: 1,
5
5
  };