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 +1 -1
- package/src/v4/classic/tests/discriminated-unions.test.ts +112 -6
- package/src/v4/classic/tests/recursive-types.test.ts +45 -0
- package/src/v4/core/compile.ts +1 -1
- package/src/v4/core/schemas.ts +36 -30
- package/src/v4/core/tests/locales/tg.test.ts +85 -0
- package/src/v4/core/versions.ts +1 -1
- package/src/v4/locales/index.ts +1 -0
- package/src/v4/locales/tg.ts +134 -0
- package/src/v4/mini/tests/recursive-types.test.ts +16 -0
- package/v4/core/compile.cjs +1 -1
- package/v4/core/compile.js +1 -1
- package/v4/core/schemas.cjs +37 -29
- package/v4/core/schemas.d.cts +8 -4
- package/v4/core/schemas.d.ts +8 -4
- package/v4/core/schemas.js +37 -29
- package/v4/core/versions.cjs +1 -1
- package/v4/core/versions.js +1 -1
- package/v4/locales/index.cjs +3 -1
- package/v4/locales/index.d.cts +1 -0
- package/v4/locales/index.d.ts +1 -0
- package/v4/locales/index.js +1 -0
- package/v4/locales/tg.cjs +143 -0
- package/v4/locales/tg.d.cts +5 -0
- package/v4/locales/tg.d.ts +4 -0
- package/v4/locales/tg.js +116 -0
package/package.json
CHANGED
|
@@ -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 = [
|
|
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
|
-
//
|
|
855
|
+
// ambiguous absence does not prevent explicit tags from routing
|
|
831
856
|
for (const k of omittable) {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
).
|
|
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",
|
package/src/v4/core/compile.ts
CHANGED
|
@@ -1869,7 +1869,7 @@ function generateDiscriminatedUnionCheck(
|
|
|
1869
1869
|
throw new ZodCompileUnsupportedError("discriminated union option without static discriminator values");
|
|
1870
1870
|
}
|
|
1871
1871
|
|
|
1872
|
-
//
|
|
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)}`);
|
package/src/v4/core/schemas.ts
CHANGED
|
@@ -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
|
-
:
|
|
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
|
-
:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2719
|
-
|
|
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
|
+
});
|
package/src/v4/core/versions.ts
CHANGED
package/src/v4/locales/index.ts
CHANGED
|
@@ -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",
|
package/v4/core/compile.cjs
CHANGED
|
@@ -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
|
-
//
|
|
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)}`);
|
package/v4/core/compile.js
CHANGED
|
@@ -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
|
-
//
|
|
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)}`);
|