zod 4.6.0 → 4.6.2

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.6.0",
3
+ "version": "4.6.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "author": "Colin McDonnell <zod@colinhacks.com>",
@@ -740,6 +740,26 @@ test("encode with codec discriminator", () => {
740
740
  expect(encoded).toEqual({ type: 1, value: "hello" });
741
741
  });
742
742
 
743
+ test("nested encoding does not select a sibling from forward discriminator values", async () => {
744
+ const tag = (value: "a" | "b") =>
745
+ z.codec(z.literal(value).default(value), z.undefined(), {
746
+ decode: () => undefined,
747
+ encode: () => value,
748
+ });
749
+ const inner = z.discriminatedUnion("type", [
750
+ z.object({ type: tag("a"), value: z.literal("a") }),
751
+ z.object({ type: tag("b"), value: z.literal("b") }),
752
+ ]);
753
+ const outer = z.discriminatedUnion("type", [inner, z.object({ type: z.undefined(), value: z.string() })]);
754
+ for (const value of ["a", "b"] as const) {
755
+ const input = { type: undefined, value };
756
+ const expected = { type: value, value };
757
+ expect(z.encode(inner, input)).toEqual(expected);
758
+ expect(z.encode(outer, input)).toEqual(expected);
759
+ expect(await z.encodeAsync(outer, input)).toEqual(expected);
760
+ }
761
+ });
762
+
743
763
  test("getDiscriminatedOption", () => {
744
764
  const fruit = z.object({ type: z.literal("fruit"), seeds: z.boolean() });
745
765
  const veg = z.object({ type: z.literal("vegetable"), leafy: z.boolean() });
@@ -811,9 +831,14 @@ test.each(["__proto__", "constructor", "toString", "hasOwnProperty", "valueOf"])
811
831
  }
812
832
  );
813
833
 
814
- // An omittable discriminator reads back as undefined at the lookup, exactly as TypeScript sees it: `{ k?: "a" } | { k?: "c" }` does not narrow on `k === undefined`.
815
834
  test("an omittable discriminator claims undefined", () => {
816
- const omittable = [z.exactOptional(z.literal("a")), z.optional(z.literal("a")), z.literal("a").default("a")];
835
+ const omittable = [
836
+ z.exactOptional(z.literal("a")),
837
+ z.optional(z.literal("a")),
838
+ z.literal("a").default("a"),
839
+ z.literal("a").prefault("a"),
840
+ z.literal("a").catch("a"),
841
+ ];
817
842
  for (const k of omittable) {
818
843
  expect(z.object({ k })._zod.propValues.k).toEqual(new Set(["a", undefined]));
819
844
  }
@@ -827,10 +852,91 @@ test("an omittable discriminator claims undefined", () => {
827
852
  expect(z.union(options).safeParse({ x: "s" }).success).toEqual(true);
828
853
  expect(z.discriminatedUnion("k", options).safeParse({ k: "b", y: 1 }).success).toEqual(true);
829
854
 
830
- // two options omit the key: both claim undefined, so they are not discriminable on it
855
+ // ambiguous absence does not prevent explicit tags from routing
831
856
  for (const k of omittable) {
832
- expect(() =>
833
- z.discriminatedUnion("k", [z.object({ k }), z.object({ k: z.exactOptional(z.literal("c")) })]).parse({})
834
- ).toThrow(/Duplicate discriminator value "undefined"/);
857
+ const schema = z.discriminatedUnion("k", [z.object({ k }), z.object({ k: z.exactOptional(z.literal("c")) })]);
858
+ expect(schema.parse({ k: "a" })).toEqual({ k: "a" });
859
+ expect(schema.parse({ k: "c" })).toEqual({ k: "c" });
860
+ expect(schema.safeParse({}).success).toBe(false);
861
+ }
862
+ });
863
+
864
+ test("defaulted discriminators preserve tagged parsing without guessing a member", async () => {
865
+ const a = z.object({ type: z.literal("a").default("a"), x: z.number().positive() });
866
+ const b = z.object({ type: z.literal("b").default("b"), y: z.string() });
867
+ const c = z.object({ type: z.literal("c").default("c"), z: z.boolean() });
868
+ for (const options of [
869
+ [a, b, c],
870
+ [c, b, a],
871
+ ] as const) {
872
+ const schema = z.discriminatedUnion("type", options);
873
+ for (const input of [a.parse({ x: 1 }), b.parse({ y: "s" }), c.parse({ z: true })]) {
874
+ expect(schema.parse(input)).toEqual(input);
875
+ expect((await schema.safeParseAsync(input)).success).toBe(true);
876
+ }
877
+ for (const input of [{ x: 1, y: "s", z: true }, { type: undefined }, { type: "other" }, { type: "a", x: -1 }]) {
878
+ expect(schema.safeParse(input).success).toBe(false);
879
+ expect((await schema.safeParseAsync(input)).success).toBe(false);
880
+ }
881
+ const result = schema.safeParse({});
882
+ expect(result.error?.issues[0]).toMatchObject({
883
+ code: "invalid_union",
884
+ path: ["type"],
885
+ options: options.map((o) => o.shape.type.unwrap().value),
886
+ });
887
+ expect(z.getDiscriminatedOption(schema, "a")).toBe(a);
888
+ }
889
+ const unique = z.discriminatedUnion("type", [a, b.safeExtend({ type: b.shape.type.unwrap() })]);
890
+ expect(unique.parse({ x: 1 })).toEqual({ type: "a", x: 1 });
891
+ });
892
+
893
+ test("undefined collisions are value-based and discriminator lookup rejects ambiguity", () => {
894
+ const a = z.object({ type: z.literal("a").optional() });
895
+ const absent = z.object({ type: z.undefined() });
896
+ for (const options of [
897
+ [a, absent],
898
+ [absent, a],
899
+ [absent, absent, a],
900
+ ] as const) {
901
+ const schema = z.discriminatedUnion("type", options);
902
+ expect(schema.parse({ type: "a" })).toEqual({ type: "a" });
903
+ expect(schema.safeParse({}).success).toBe(false);
904
+ expect(() => z.getDiscriminatedOption(schema, undefined)).toThrow('Ambiguous discriminator value "undefined"');
905
+ }
906
+ const unique = z.discriminatedUnion("type", [absent, z.object({ type: z.literal("a") })]);
907
+ expect(unique.parse({ type: undefined })).toEqual({ type: undefined });
908
+ expect(z.getDiscriminatedOption(unique, undefined)).toBe(absent);
909
+ });
910
+
911
+ test("non-undefined discriminator collisions remain schema errors", () => {
912
+ for (const tag of [z.literal("a").default("a"), z.literal(["a", "b"]), z.literal("a").nullable()]) {
913
+ const schema = z.discriminatedUnion("type", [z.object({ type: tag }), z.object({ type: tag })]);
914
+ expect(() => schema.safeParse({ type: "a" })).toThrow(/Duplicate discriminator value/);
915
+ expect(() => z.encode(schema, { type: "a" })).toThrow(/Duplicate discriminator value/);
916
+ expect(() => z.getDiscriminatedOption(schema, "a")).toThrow(/Duplicate discriminator value/);
917
+ }
918
+ const nullable = z.discriminatedUnion("type", [
919
+ z.object({ type: z.literal("a").nullable() }),
920
+ z.object({ type: z.literal("b").nullable() }),
921
+ ]);
922
+ expect(() => nullable.safeParse({ type: "a" })).toThrow('Duplicate discriminator value "null"');
923
+ });
924
+
925
+ test("nested defaulted unions only advertise routable discriminator values", () => {
926
+ const a = z.object({ type: z.literal("a").default("a"), group: z.literal("inner") });
927
+ const b = z.object({ type: z.literal("b").default("b"), group: z.literal("inner") });
928
+ const inner = z.discriminatedUnion("type", [a, b]);
929
+ const c = z.object({ type: z.literal("c").default("c"), group: z.literal("outer") });
930
+ expect(inner._zod.propValues.type).toEqual(new Set(["a", "b"]));
931
+ for (const schema of [
932
+ z.discriminatedUnion("type", [z.lazy(() => inner), c]),
933
+ z.discriminatedUnion("group", [inner, c]),
934
+ ]) {
935
+ expect(schema.parse({ type: "a", group: "inner" })).toEqual({ type: "a", group: "inner" });
936
+ expect(schema.parse({ group: "outer" })).toEqual({ type: "c", group: "outer" });
937
+ expect(schema.safeParse({ group: "inner" }).success).toBe(false);
835
938
  }
939
+ const fallback = z.discriminatedUnion("type", [a, b], { unionFallback: true });
940
+ expect(fallback._zod.propValues.type.has(undefined)).toBe(true);
941
+ expect(fallback.parse({ group: "inner" })).toEqual({ type: "a", group: "inner" });
836
942
  });
@@ -1,6 +1,76 @@
1
1
  import { expect, expectTypeOf, test } from "vitest";
2
2
  import { z } from "zod/v4";
3
3
 
4
+ test.each([false, true])("undefined prefault preserves object keys (jitless: %s)", async (jitless) => {
5
+ const previous = z.config().jitless;
6
+ z.config({ jitless });
7
+ try {
8
+ let calls = 0;
9
+ const field = z.union([z.string(), z.undefined()]).prefault(() => {
10
+ calls++;
11
+ return undefined;
12
+ });
13
+ const schema = z.object({ a: field });
14
+ expectTypeOf<z.output<typeof field>>().toEqualTypeOf<string | undefined>();
15
+ expectTypeOf<z.input<typeof schema>>().toEqualTypeOf<{ a?: string | undefined }>();
16
+ expectTypeOf<z.output<typeof schema>>().toEqualTypeOf<{ a: string | undefined }>();
17
+ expect(schema.parse({})).toStrictEqual({ a: undefined });
18
+ expect(calls).toBe(1);
19
+ expect(schema.parse({ a: undefined })).toStrictEqual({ a: undefined });
20
+ expect(calls).toBe(2);
21
+ expect(await schema.parseAsync({})).toStrictEqual({ a: undefined });
22
+ expect(calls).toBe(3);
23
+ expect(schema.parse({ a: "value" })).toStrictEqual({ a: "value" });
24
+ expect(schema.safeParse({ a: 123 }).success).toBe(false);
25
+ expect(calls).toBe(3);
26
+
27
+ const literal = z.prefault(z.string().optional(), undefined);
28
+ expect(z.core.compileFn(z.object({ a: literal }))({})).toStrictEqual({ a: undefined });
29
+ for (const wrapped of [literal, literal.readonly(), literal.nullable(), z.lazy(() => literal)]) {
30
+ expect(z.object({ a: wrapped }).parse({})).toStrictEqual({ a: undefined });
31
+ }
32
+ expect(z.object({ a: literal.optional() }).parse({})).toStrictEqual({});
33
+ expect(z.object({ a: literal.optional() }).parse({ a: undefined })).toStrictEqual({ a: undefined });
34
+ expect(z.tuple([literal]).parse([])).toStrictEqual([undefined]);
35
+ } finally {
36
+ z.config({ jitless: previous });
37
+ }
38
+ });
39
+
40
+ test("prefault preserves transformed undefined output", async () => {
41
+ const field = z
42
+ .string()
43
+ .transform(() => undefined)
44
+ .prefault("fallback");
45
+ expectTypeOf<z.output<typeof field>>().toEqualTypeOf<undefined>();
46
+ expect(field.parse(undefined)).toBeUndefined();
47
+ const schema = z.object({ a: field, optional: z.string().optional() });
48
+ expectTypeOf<z.output<typeof schema>>().toEqualTypeOf<{ a: undefined; optional?: string | undefined }>();
49
+ expect(schema.parse({})).toStrictEqual({ a: undefined });
50
+ expect(z.core.compileFn(schema)({})).toStrictEqual({ a: undefined });
51
+ expect(z.compile(schema).parse({})).toStrictEqual({ a: undefined });
52
+ expect(
53
+ await z
54
+ .object({
55
+ a: z
56
+ .string()
57
+ .transform(async () => undefined)
58
+ .prefault("fallback"),
59
+ })
60
+ .parseAsync({})
61
+ ).toStrictEqual({ a: undefined });
62
+ expect(
63
+ z
64
+ .object({
65
+ a: z
66
+ .string()
67
+ .prefault("fallback")
68
+ .transform(() => undefined),
69
+ })
70
+ .parse({})
71
+ ).toStrictEqual({ a: undefined });
72
+ });
73
+
4
74
  test("basic prefault", () => {
5
75
  const a = z.prefault(z.string().trim(), " default ");
6
76
  expect(a).toBeInstanceOf(z.ZodPrefault);
@@ -1,6 +1,51 @@
1
1
  import { expect, expectTypeOf, test } from "vitest";
2
2
  import { z } from "zod/v4";
3
3
 
4
+ test("recursive object schema type aliases", () => {
5
+ type ObjectField = z.ZodObject<{ [k: string]: ObjectField }>;
6
+ type MixedField = z.ZodString | z.ZodObject<{ [k: string]: MixedField }>;
7
+ expectTypeOf<ObjectField>().toExtend<z.ZodType>();
8
+ expectTypeOf<MixedField>().toExtend<z.ZodType>();
9
+
10
+ type ObjectValue = { [k: string]: ObjectValue };
11
+ type MixedValue = string | { [k: string]: MixedValue };
12
+ expectTypeOf<z.input<ObjectField>>().toEqualTypeOf<ObjectValue>();
13
+ expectTypeOf<z.output<ObjectField>>().toEqualTypeOf<ObjectValue>();
14
+ expectTypeOf<z.input<MixedField>>().toEqualTypeOf<MixedValue>();
15
+ expectTypeOf<z.output<MixedField>>().toEqualTypeOf<MixedValue>();
16
+
17
+ const schema: MixedField = z.object({ name: z.string(), nested: z.object({ name: z.string() }) });
18
+ expect(schema.parse({ name: "a", nested: { name: "b", extra: true } })).toEqual({
19
+ name: "a",
20
+ nested: { name: "b" },
21
+ });
22
+ expect(schema.safeParse({ name: "a", nested: { name: 123 } }).success).toBe(false);
23
+
24
+ type CoreField = z.core.$ZodString<string> | z.core.$ZodObject<{ [k: string]: CoreField }>;
25
+ expectTypeOf<z.input<CoreField>>().toEqualTypeOf<MixedValue>();
26
+ expectTypeOf<z.output<CoreField>>().toEqualTypeOf<MixedValue>();
27
+ });
28
+
29
+ test("recursive object aliases preserve distinct input and output", () => {
30
+ type Field = z.ZodPipe<z.ZodString, z.ZodTransform<number, string>> | z.ZodObject<{ [k: string]: Field }>;
31
+ type Input = string | { [k: string]: Input };
32
+ type Output = number | { [k: string]: Output };
33
+ expectTypeOf<z.input<Field>>().toEqualTypeOf<Input>();
34
+ expectTypeOf<z.output<Field>>().toEqualTypeOf<Output>();
35
+ const schema: Field = z.object({ nested: z.object({ length: z.string().transform((s) => s.length) }) });
36
+ expect(schema.parse({ nested: { length: "abc" } })).toEqual({ nested: { length: 3 } });
37
+ expect(schema.safeParse({ nested: { length: 3 } }).success).toBe(false);
38
+ });
39
+
40
+ test("indexed object inference preserves string keys", () => {
41
+ type Indexed = z.ZodObject<Record<string, z.ZodString>>;
42
+ type Recursive = z.ZodObject<Record<string, Recursive>>;
43
+ expectTypeOf<keyof z.input<Indexed>>().toEqualTypeOf<string>();
44
+ expectTypeOf<keyof z.output<Indexed>>().toEqualTypeOf<string>();
45
+ expectTypeOf<keyof z.input<Recursive>>().toEqualTypeOf<string>();
46
+ expectTypeOf<keyof z.output<Recursive>>().toEqualTypeOf<string>();
47
+ });
48
+
4
49
  test("recursion with z.lazy", () => {
5
50
  const data = {
6
51
  name: "I",
@@ -1270,9 +1270,9 @@ function generateObjectCheck(
1270
1270
  }
1271
1271
  // else: strip mode (no catchall) - unknown keys ignored, only include known keys
1272
1272
 
1273
- // Shape keys in declared order, then unknown keys in for...in order. A middle-rung key is included iff present on the input, else iff its output is not undefined.
1273
+ // defaulted required outputs keep their keys even when undefined
1274
1274
  const outputVar = newVar(ctx);
1275
- const hasConditionalKeys = allKeys.some((k) => mayOutputUndefined(propShape[k]!) || dropsWhenAbsent(propShape[k]!));
1275
+ const hasConditionalKeys = allKeys.some((k) => mayOmitUndefined(propShape[k]!) || dropsWhenAbsent(propShape[k]!));
1276
1276
 
1277
1277
  // Assert mode: every declared key is validated above, so the output literal and the unknown-key copy are pure waste. A `never` catchall already emitted its rejection loop; a schema catchall still has to validate the values it would otherwise have stored.
1278
1278
  if (!buildsValue) {
@@ -1301,7 +1301,7 @@ function generateObjectCheck(
1301
1301
  const out = propOutputs.get(k);
1302
1302
  if (dropsWhenAbsent(propShape[k]!)) {
1303
1303
  doc.write(`if (${kx} in ${accessor}) ${outputVar}[${kx}] = ${out};`);
1304
- } else if (mayOutputUndefined(propShape[k]!)) {
1304
+ } else if (mayOmitUndefined(propShape[k]!)) {
1305
1305
  doc.write(`if (${out} !== undefined || ${kx} in ${accessor}) ${outputVar}[${kx}] = ${out};`);
1306
1306
  } else {
1307
1307
  doc.write(`${outputVar}[${kx}] = ${out};`);
@@ -1464,9 +1464,11 @@ function dropsWhenAbsent(schema: SomeType): boolean {
1464
1464
  return schema._zod.optin === "optional" && schema._zod.optout === "optional";
1465
1465
  }
1466
1466
 
1467
- // Whether a schema's success-path output can be `undefined`. Object output
1468
- // assembly gives such props the runtime's value-or-presence inclusion rule;
1469
- // everything else keeps the unconditional object-literal slot.
1467
+ function mayOmitUndefined(schema: SomeType): boolean {
1468
+ return (schema._zod.optin !== "defaulted" || schema._zod.optout === "optional") && mayOutputUndefined(schema);
1469
+ }
1470
+
1471
+ // whether a schema's success-path output can be undefined
1470
1472
  function mayOutputUndefined(schema: SomeType): boolean {
1471
1473
  const def = schema._zod.def as {
1472
1474
  type: string;
@@ -1869,7 +1871,7 @@ function generateDiscriminatedUnionCheck(
1869
1871
  throw new ZodCompileUnsupportedError("discriminated union option without static discriminator values");
1870
1872
  }
1871
1873
 
1872
- // Two options claiming one value are not discriminable, and the branch chain below would silently give it to the first. Declining to compile hands that back to the interpreter, whose own map build reports it.
1874
+ // let the interpreter handle collisions instead of compiling first-match dispatch
1873
1875
  for (const value of values) {
1874
1876
  if (claimed.has(value)) {
1875
1877
  throw new ZodCompileUnsupportedError(`duplicate discriminator value ${String(value)}`);
@@ -1893,7 +1893,7 @@ type OptionalInSchema = { _zod: { optin: "optional" | "defaulted" } };
1893
1893
  export type $InferObjectOutput<T extends $ZodLooseShape, Extra extends Record<string, unknown>> = string extends keyof T
1894
1894
  ? util.IsAny<T[keyof T]> extends true
1895
1895
  ? Record<string, unknown>
1896
- : Record<string, core.output<T[keyof T]>>
1896
+ : { [k in string]: core.output<T[keyof T]> }
1897
1897
  : keyof (T & Extra) extends never
1898
1898
  ? Record<string, never>
1899
1899
  : util.Prettify<
@@ -1937,7 +1937,7 @@ export type $InferObjectOutput<T extends $ZodLooseShape, Extra extends Record<st
1937
1937
  export type $InferObjectInput<T extends $ZodLooseShape, Extra extends Record<string, unknown>> = string extends keyof T
1938
1938
  ? util.IsAny<T[keyof T]> extends true
1939
1939
  ? Record<string, unknown>
1940
- : Record<string, core.input<T[keyof T]>>
1940
+ : { [k in string]: core.input<T[keyof T]> }
1941
1941
  : keyof (T & Extra) extends never
1942
1942
  ? Record<string, never>
1943
1943
  : util.Prettify<
@@ -1983,7 +1983,7 @@ function handlePropertyResult(
1983
1983
  }
1984
1984
 
1985
1985
  if (result.value === undefined) {
1986
- if (isPresent) {
1986
+ if (isPresent || (optin === "defaulted" && !isOptionalOut)) {
1987
1987
  (final.value as any)[key] = undefined;
1988
1988
  }
1989
1989
  } else {
@@ -2314,16 +2314,16 @@ export const $ZodObjectJIT: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$
2314
2314
  doc.write(`
2315
2315
  if (${id}.issues.length) {${prefixStr(id, k)}
2316
2316
  }
2317
-
2318
- if (${id}.value === undefined) {
2319
- if (${isPresent}) {
2320
- newResult[${k}] = undefined;
2321
- }
2322
- } else {
2317
+ `);
2318
+ if (optin === "defaulted") {
2319
+ doc.write(`newResult[${k}] = ${id}.value;`);
2320
+ } else {
2321
+ doc.write(`
2322
+ if (${id}.value !== undefined || ${isPresent}) {
2323
2323
  newResult[${k}] = ${id}.value;
2324
2324
  }
2325
-
2326
2325
  `);
2326
+ }
2327
2327
  }
2328
2328
  }
2329
2329
 
@@ -2600,7 +2600,7 @@ export interface $ZodDiscriminatedUnionInternals<
2600
2600
  def: $ZodDiscriminatedUnionDef<Options, Disc>;
2601
2601
  propValues: util.PropValues;
2602
2602
  bag: util.LoosePartial<{
2603
- optionsMap: Map<util.Primitive, $ZodType>;
2603
+ optionsMap: Map<util.Primitive, $ZodType | null>;
2604
2604
  }>;
2605
2605
  }
2606
2606
 
@@ -2624,7 +2624,7 @@ export type $DiscriminatedOption<Options extends readonly SomeType[], Disc exten
2624
2624
  : never;
2625
2625
  }[number];
2626
2626
 
2627
- /** Returns the option of `union` whose discriminator claims `value`. */
2627
+ /** Returns the option whose discriminator claims `value`, or throws if ambiguous. */
2628
2628
  export function getDiscriminatedOption<
2629
2629
  Options extends readonly SomeType[],
2630
2630
  Disc extends string,
@@ -2633,15 +2633,31 @@ export function getDiscriminatedOption<
2633
2633
  const internals = union._zod;
2634
2634
  let map = internals.bag.optionsMap;
2635
2635
  if (!map) {
2636
- map = new Map();
2637
- const { options, discriminator } = internals.def;
2638
- for (const option of options as unknown as readonly $ZodType[]) {
2639
- // First declaration wins, matching the order the parse path resolves a duplicate in.
2640
- for (const v of option._zod.propValues?.[discriminator] ?? []) if (!map.has(v)) map.set(v, option);
2641
- }
2636
+ map = discriminatorMap(internals.def);
2642
2637
  internals.bag.optionsMap = map;
2643
2638
  }
2644
- return map.get(value as util.Primitive) as any;
2639
+ const option = map.get(value as util.Primitive);
2640
+ if (option === null) throw new Error(`Ambiguous discriminator value "${String(value)}"`);
2641
+ return option as any;
2642
+ }
2643
+
2644
+ function discriminatorMap(def: $ZodDiscriminatedUnionDef<readonly SomeType[]>): Map<util.Primitive, $ZodType | null> {
2645
+ const map = new Map<util.Primitive, $ZodType | null>();
2646
+ for (const option of def.options as readonly $ZodType[]) {
2647
+ const values = option._zod.propValues?.[def.discriminator];
2648
+ if (!values || values.size === 0)
2649
+ throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(option)}"`);
2650
+ for (const value of values) {
2651
+ if (map.has(value)) {
2652
+ if (value !== undefined) throw new Error(`Duplicate discriminator value "${String(value)}"`);
2653
+ // keep the collision marked so a later member cannot reclaim it
2654
+ map.set(value, null);
2655
+ } else {
2656
+ map.set(value, option);
2657
+ }
2658
+ }
2659
+ }
2660
+ return map;
2645
2661
  }
2646
2662
 
2647
2663
  export interface $ZodDiscriminatedUnion<
@@ -2661,10 +2677,12 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
2661
2677
  const _super = inst._zod.parse;
2662
2678
  util.defineLazyInternal(inst, "propValues", (zod) => {
2663
2679
  const propValues: util.PropValues = {};
2680
+ let undefinedCount = 0;
2664
2681
  for (const option of zod.def.options) {
2665
2682
  const pv = option._zod.propValues;
2666
2683
  if (!pv || Object.keys(pv).length === 0)
2667
2684
  throw new Error(`Invalid discriminated union option at index "${zod.def.options.indexOf(option)}"`);
2685
+ if (pv[zod.def.discriminator]?.has(undefined)) undefinedCount++;
2668
2686
  for (const [k, v] of Object.entries(pv!)) {
2669
2687
  if (!Object.prototype.hasOwnProperty.call(propValues, k)) {
2670
2688
  util.assignProp(propValues, k, new Set());
@@ -2674,6 +2692,7 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
2674
2692
  }
2675
2693
  }
2676
2694
  }
2695
+ if (!zod.def.unionFallback && undefinedCount > 1) propValues[zod.def.discriminator]?.delete(undefined);
2677
2696
  return propValues;
2678
2697
  });
2679
2698
 
@@ -2685,22 +2704,7 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
2685
2704
  }
2686
2705
  });
2687
2706
 
2688
- const disc = util.cached(() => {
2689
- const opts = def.options;
2690
- const map: Map<util.Primitive, $ZodType> = new Map();
2691
- for (const o of opts) {
2692
- const values = o._zod.propValues?.[def.discriminator];
2693
- if (!values || values.size === 0)
2694
- throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(o)}"`);
2695
- for (const v of values) {
2696
- if (map.has(v)) {
2697
- throw new Error(`Duplicate discriminator value "${String(v)}"`);
2698
- }
2699
- map.set(v, o);
2700
- }
2701
- }
2702
- return map;
2703
- });
2707
+ const disc = util.cached(() => discriminatorMap(def));
2704
2708
 
2705
2709
  inst._zod.parse = (payload, ctx) => {
2706
2710
  const input = payload.value;
@@ -2715,8 +2719,10 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
2715
2719
  return payload;
2716
2720
  }
2717
2721
 
2718
- const opt = disc.value.get(input?.[def.discriminator] as any);
2719
- if (opt) {
2722
+ const value = input?.[def.discriminator];
2723
+ const opt = disc.value.get(value as util.Primitive);
2724
+ // forward metadata cannot choose an encoder for an absent tag
2725
+ if (opt && (value !== undefined || ctx.direction !== "backward")) {
2720
2726
  return opt._zod.run(payload, ctx) as any;
2721
2727
  }
2722
2728
 
@@ -2733,7 +2739,7 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
2733
2739
  errors: [],
2734
2740
  note: "No matching discriminator",
2735
2741
  discriminator: def.discriminator,
2736
- options: Array.from(disc.value.keys()),
2742
+ options: Array.from(disc.value.keys()).filter((value) => disc.value.get(value) !== null),
2737
2743
  input,
2738
2744
  path: [def.discriminator],
2739
2745
  inst,
@@ -4071,7 +4077,7 @@ export interface $ZodPrefaultDef<T extends SomeType = $ZodType> extends $ZodType
4071
4077
  }
4072
4078
 
4073
4079
  export interface $ZodPrefaultInternals<T extends SomeType = $ZodType>
4074
- extends $ZodTypeInternals<util.NoUndefined<core.output<T>>, core.input<T> | undefined> {
4080
+ extends $ZodTypeInternals<core.output<T>, core.input<T> | undefined> {
4075
4081
  def: $ZodPrefaultDef<T>;
4076
4082
  optin: "defaulted";
4077
4083
  optout?: "optional" | undefined;
@@ -0,0 +1,85 @@
1
+ import { expect, test } from "vitest";
2
+ import * as z from "zod/v4";
3
+
4
+ test("locales - tg", () => {
5
+ z.config(z.locales.tg());
6
+
7
+ const invalidType = z.number().safeParse("a");
8
+ expect(invalidType.error!.issues[0].code).toBe("invalid_type");
9
+ expect(invalidType.error!.issues[0].message).toBe("Вуруди нодуруст: рақам интизор мерафт, сатр гирифта шуд");
10
+
11
+ const invalidType2 = z.string().safeParse(1);
12
+ expect(invalidType2.error!.issues[0].code).toBe("invalid_type");
13
+ expect(invalidType2.error!.issues[0].message).toBe("Вуруди нодуруст: сатр интизор мерафт, рақам гирифта шуд");
14
+
15
+ const invalidValue = z.enum(["a", "b"]).safeParse(1);
16
+ expect(invalidValue.error!.issues[0].code).toBe("invalid_value");
17
+ expect(invalidValue.error!.issues[0].message).toBe('Интихоби нодуруст: яке аз "a"|"b" интизор мерафт');
18
+
19
+ const tooBig = z.number().max(10).safeParse(15);
20
+ expect(tooBig.error!.issues[0].code).toBe("too_big");
21
+ expect(tooBig.error!.issues[0].message).toBe("Хеле калон: number бояд <=10 бошад");
22
+
23
+ const tooSmall = z.number().min(10).safeParse(5);
24
+ expect(tooSmall.error!.issues[0].code).toBe("too_small");
25
+ expect(tooSmall.error!.issues[0].message).toBe("Хеле хурд: number бояд >=10 бошад");
26
+
27
+ // singular units after numerals in Tajik
28
+ const tooShort = z.string().min(5).safeParse("hi");
29
+ expect(tooShort.error!.issues[0].message).toBe("Хеле хурд: string бояд >=5 аломат дошта бошад");
30
+
31
+ const tooFewItems = z.array(z.number()).min(2).safeParse([1]);
32
+ expect(tooFewItems.error!.issues[0].message).toBe("Хеле хурд: array бояд >=2 унсур дошта бошад");
33
+
34
+ const invalidEmail = z.string().email().safeParse("nope");
35
+ expect(invalidEmail.error!.issues[0].code).toBe("invalid_format");
36
+ expect(invalidEmail.error!.issues[0].message).toBe("суроғаи email-и нодуруст");
37
+
38
+ const invalidStart = z.string().startsWith("ab").safeParse("xy");
39
+ expect(invalidStart.error!.issues[0].message).toBe('Сатри нодуруст: бояд бо "ab" оғоз шавад');
40
+
41
+ const invalidRegex = z.string().regex(/abcd/).safeParse("xy");
42
+ expect(invalidRegex.error!.issues[0].message).toContain("мувофиқат кунад");
43
+
44
+ const notMultipleOf = z.number().multipleOf(5).safeParse(7);
45
+ expect(notMultipleOf.error!.issues[0].message).toBe("Рақами нодуруст: бояд ба 5 бе бақия тақсим шавад");
46
+
47
+ const oneUnknownKey = z.strictObject({ a: z.string() }).safeParse({ a: "x", b: 1 });
48
+ expect(oneUnknownKey.error!.issues[0].message).toBe('Калиди номаълум: "b"');
49
+
50
+ const twoUnknownKeys = z.strictObject({ a: z.string() }).safeParse({ a: "x", b: 1, c: 2 });
51
+ expect(twoUnknownKeys.error!.issues[0].message).toBe('Калидҳои номаълум: "b", "c"');
52
+
53
+ const cases: [z.ZodType, unknown, string][] = [
54
+ [z.literal("a"), "b", 'Вуруди нодуруст: "a" интизор мерафт'],
55
+ [z.boolean(), null, "Вуруди нодуруст: boolean интизор мерафт, null гирифта шуд"],
56
+ [z.number(), Number.NaN, "Вуруди нодуруст: рақам интизор мерафт, NaN гирифта шуд"],
57
+ [z.number().lt(10), 10, "Хеле калон: number бояд <10 бошад"],
58
+ [z.number().gt(10), 10, "Хеле хурд: number бояд >10 бошад"],
59
+ [z.string().max(1), "ab", "Хеле калон: string бояд <=1 аломат дошта бошад"],
60
+ [z.set(z.string()).min(1), new Set(), "Хеле хурд: set бояд >=1 унсур дошта бошад"],
61
+ [z.map(z.string(), z.number()).min(1), new Map(), "Хеле хурд: map бояд >=1 сабт дошта бошад"],
62
+ [z.file().min(1), new File([], "empty"), "Хеле хурд: file бояд >=1 байт дошта бошад"],
63
+ [z.string().endsWith("ab"), "xy", 'Сатри нодуруст: бояд бо "ab" анҷом ёбад'],
64
+ [z.string().includes("ab"), "xy", 'Сатри нодуруст: бояд "ab"-ро дар бар гирад'],
65
+ [z.stringFormat("custom", () => false), "xy", "custom-и нодуруст"],
66
+ [z.union([z.string(), z.number()]), null, "Вуруди нодуруст"],
67
+ [
68
+ z.discriminatedUnion("kind", [z.object({ kind: z.literal("a") }), z.object({ kind: z.literal("b") })]),
69
+ { kind: "c" },
70
+ "Қимати нодурусти дискриминатор: 'a' | 'b' интизор мерафт",
71
+ ],
72
+ [z.record(z.string().min(2), z.number()), { a: 1 }, "Калиди нодуруст дар record"],
73
+ [z.map(z.string(), z.number()), new Map([[{}, 1]]), "Калиди нодуруст дар map"],
74
+ [z.map(z.object({}), z.number()), new Map([[{}, "a"]]), "Қимати нодуруст дар map"],
75
+ [z.custom(() => false), "a", "Вуруди нодуруст"],
76
+ ];
77
+ for (const [schema, input, message] of cases) {
78
+ expect(schema.safeParse(input).error!.issues[0].message).toBe(message);
79
+ }
80
+
81
+ expect(z.object({ name: z.string().min(1), age: z.number().min(0) }).parse({ name: "Ali", age: 30 })).toEqual({
82
+ name: "Ali",
83
+ age: 30,
84
+ });
85
+ });
@@ -1,5 +1,5 @@
1
1
  export const version = {
2
2
  major: 4,
3
3
  minor: 6,
4
- patch: 0 as number,
4
+ patch: 2 as number,
5
5
  } as const;
@@ -49,6 +49,7 @@ export { default as sk } from "./sk.js";
49
49
  export { default as sl } from "./sl.js";
50
50
  export { default as sv } from "./sv.js";
51
51
  export { default as ta } from "./ta.js";
52
+ export { default as tg } from "./tg.js";
52
53
  export { default as th } from "./th.js";
53
54
  export { default as tk } from "./tk.js";
54
55
  export { default as tr } from "./tr.js";