zod 4.6.0 → 4.6.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.6.0",
3
+ "version": "4.6.1",
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,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",
@@ -1869,7 +1869,7 @@ function generateDiscriminatedUnionCheck(
1869
1869
  throw new ZodCompileUnsupportedError("discriminated union option without static discriminator values");
1870
1870
  }
1871
1871
 
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.
1872
+ // let the interpreter handle collisions instead of compiling first-match dispatch
1873
1873
  for (const value of values) {
1874
1874
  if (claimed.has(value)) {
1875
1875
  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<
@@ -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,
@@ -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: 1 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";
@@ -0,0 +1,134 @@
1
+ import type { $ZodStringFormats } from "../core/checks.js";
2
+ import type * as errors from "../core/errors.js";
3
+ import * as util from "../core/util.js";
4
+
5
+ const error: () => errors.$ZodErrorMap = () => {
6
+ // singular units after numerals in Tajik
7
+ const Sizable: Record<string, { unit: string; verb: string }> = {
8
+ string: { unit: "аломат", verb: "дошта бошад" },
9
+ file: { unit: "байт", verb: "дошта бошад" },
10
+ array: { unit: "унсур", verb: "дошта бошад" },
11
+ set: { unit: "унсур", verb: "дошта бошад" },
12
+ map: { unit: "сабт", verb: "дошта бошад" },
13
+ };
14
+
15
+ function getSizing(origin: string): { unit: string; verb: string } | null {
16
+ return Sizable[origin] ?? null;
17
+ }
18
+
19
+ const FormatDictionary: {
20
+ [k in $ZodStringFormats | (string & {})]?: string;
21
+ } = {
22
+ regex: "вуруд",
23
+ email: "суроғаи email",
24
+ url: "URL",
25
+ emoji: "эмоҷи",
26
+ uuid: "UUID",
27
+ uuidv4: "UUIDv4",
28
+ uuidv6: "UUIDv6",
29
+ nanoid: "nanoid",
30
+ guid: "GUID",
31
+ cuid: "cuid",
32
+ cuid2: "cuid2",
33
+ ulid: "ULID",
34
+ xid: "XID",
35
+ ksuid: "KSUID",
36
+ datetime: "санаву вақти ISO",
37
+ date: "санаи ISO",
38
+ time: "вақти ISO",
39
+ duration: "давомнокии ISO",
40
+ ipv4: "суроғаи IPv4",
41
+ ipv6: "суроғаи IPv6",
42
+ mac: "суроғаи MAC",
43
+ cidrv4: "маҳдудаи IPv4",
44
+ cidrv6: "маҳдудаи IPv6",
45
+ base64: "сатри дар формати base64",
46
+ base64url: "сатри дар формати base64url",
47
+ json_string: "сатри JSON",
48
+ e164: "рақами E.164",
49
+ credit_card: "рақами корти кредитӣ",
50
+ iban: "IBAN",
51
+ jwt: "JWT",
52
+ template_literal: "вуруд",
53
+ };
54
+
55
+ const TypeDictionary: {
56
+ [k in errors.$ZodInvalidTypeExpected | (string & {})]?: string;
57
+ } = {
58
+ nan: "NaN",
59
+ number: "рақам",
60
+ string: "сатр",
61
+ array: "массив",
62
+ object: "объект",
63
+ date: "сана",
64
+ };
65
+
66
+ return (issue) => {
67
+ switch (issue.code) {
68
+ case "invalid_type": {
69
+ const expected = TypeDictionary[issue.expected] ?? issue.expected;
70
+ const receivedType = util.parsedType(issue.input);
71
+ const received = TypeDictionary[receivedType] ?? receivedType;
72
+ return `Вуруди нодуруст: ${expected} интизор мерафт, ${received} гирифта шуд`;
73
+ }
74
+
75
+ case "invalid_value":
76
+ if (issue.values.length === 1)
77
+ return `Вуруди нодуруст: ${util.stringifyPrimitive(issue.values[0])} интизор мерафт`;
78
+ return `Интихоби нодуруст: яке аз ${util.joinValues(issue.values, "|")} интизор мерафт`;
79
+
80
+ case "too_big": {
81
+ const adj = issue.inclusive ? "<=" : "<";
82
+ const sizing = getSizing(issue.origin);
83
+ if (sizing)
84
+ return `Хеле калон: ${issue.origin ?? "қимат"} бояд ${adj}${issue.maximum.toString()} ${sizing.unit} ${sizing.verb}`;
85
+ return `Хеле калон: ${issue.origin ?? "қимат"} бояд ${adj}${issue.maximum.toString()} бошад`;
86
+ }
87
+
88
+ case "too_small": {
89
+ const adj = issue.inclusive ? ">=" : ">";
90
+ const sizing = getSizing(issue.origin);
91
+ if (sizing)
92
+ return `Хеле хурд: ${issue.origin} бояд ${adj}${issue.minimum.toString()} ${sizing.unit} ${sizing.verb}`;
93
+ return `Хеле хурд: ${issue.origin} бояд ${adj}${issue.minimum.toString()} бошад`;
94
+ }
95
+
96
+ case "invalid_format": {
97
+ const _issue = issue as errors.$ZodStringFormatIssues;
98
+ if (_issue.format === "starts_with") return `Сатри нодуруст: бояд бо "${_issue.prefix}" оғоз шавад`;
99
+ if (_issue.format === "ends_with") return `Сатри нодуруст: бояд бо "${_issue.suffix}" анҷом ёбад`;
100
+ if (_issue.format === "includes") return `Сатри нодуруст: бояд "${_issue.includes}"-ро дар бар гирад`;
101
+ if (_issue.format === "regex") return `Сатри нодуруст: бояд ба намунаи ${_issue.pattern} мувофиқат кунад`;
102
+ return `${FormatDictionary[_issue.format] ?? issue.format}-и нодуруст`;
103
+ }
104
+
105
+ case "not_multiple_of":
106
+ return `Рақами нодуруст: бояд ба ${issue.divisor} бе бақия тақсим шавад`;
107
+
108
+ case "unrecognized_keys":
109
+ return `Калид${issue.keys.length > 1 ? "ҳои" : "и"} номаълум: ${util.joinValues(issue.keys, ", ")}`;
110
+
111
+ case "invalid_key":
112
+ return `Калиди нодуруст дар ${issue.origin}`;
113
+
114
+ case "invalid_union":
115
+ if (issue.options && Array.isArray(issue.options) && issue.options.length > 0) {
116
+ const opts = issue.options.map((o) => `'${o}'`).join(" | ");
117
+ return `Қимати нодурусти дискриминатор: ${opts} интизор мерафт`;
118
+ }
119
+ return "Вуруди нодуруст";
120
+
121
+ case "invalid_element":
122
+ return `Қимати нодуруст дар ${issue.origin}`;
123
+
124
+ default:
125
+ return `Вуруди нодуруст`;
126
+ }
127
+ };
128
+ };
129
+
130
+ export default function (): { localeError: errors.$ZodErrorMap } {
131
+ return {
132
+ localeError: error(),
133
+ };
134
+ }
@@ -1,6 +1,22 @@
1
1
  import { expect, expectTypeOf, test } from "vitest";
2
2
  import { z } from "zod/mini";
3
3
 
4
+ test("recursive object schema type aliases", () => {
5
+ type ObjectField = z.ZodMiniObject<{ [k: string]: ObjectField }>;
6
+ type Field = z.ZodMiniString<string> | z.ZodMiniObject<{ [k: string]: Field }>;
7
+ type ObjectValue = { [k: string]: ObjectValue };
8
+ type Value = string | { [k: string]: Value };
9
+ expectTypeOf<z.input<ObjectField>>().toEqualTypeOf<ObjectValue>();
10
+ expectTypeOf<z.output<ObjectField>>().toEqualTypeOf<ObjectValue>();
11
+ expectTypeOf<keyof z.input<ObjectField>>().toEqualTypeOf<string>();
12
+ expectTypeOf<keyof z.output<ObjectField>>().toEqualTypeOf<string>();
13
+ expectTypeOf<z.input<Field>>().toEqualTypeOf<Value>();
14
+ expectTypeOf<z.output<Field>>().toEqualTypeOf<Value>();
15
+ const schema: Field = z.object({ nested: z.object({ name: z.string() }) });
16
+ expect(schema.parse({ nested: { name: "a", extra: true } })).toEqual({ nested: { name: "a" } });
17
+ expect(schema.safeParse({ nested: { name: 123 } }).success).toBe(false);
18
+ });
19
+
4
20
  test("recursion with z.lazy", () => {
5
21
  const data = {
6
22
  name: "I",
@@ -1525,7 +1525,7 @@ function generateDiscriminatedUnionCheck(doc, ctx, def, accessor) {
1525
1525
  if (!values || values.size === 0) {
1526
1526
  throw new ZodCompileUnsupportedError("discriminated union option without static discriminator values");
1527
1527
  }
1528
- // 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.
1528
+ // let the interpreter handle collisions instead of compiling first-match dispatch
1529
1529
  for (const value of values) {
1530
1530
  if (claimed.has(value)) {
1531
1531
  throw new ZodCompileUnsupportedError(`duplicate discriminator value ${String(value)}`);
@@ -1494,7 +1494,7 @@ function generateDiscriminatedUnionCheck(doc, ctx, def, accessor) {
1494
1494
  if (!values || values.size === 0) {
1495
1495
  throw new ZodCompileUnsupportedError("discriminated union option without static discriminator values");
1496
1496
  }
1497
- // 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.
1497
+ // let the interpreter handle collisions instead of compiling first-match dispatch
1498
1498
  for (const value of values) {
1499
1499
  if (claimed.has(value)) {
1500
1500
  throw new ZodCompileUnsupportedError(`duplicate discriminator value ${String(value)}`);