zod 4.6.3 → 4.6.4
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/schemas.ts +9 -4
- package/src/v4/classic/tests/assignability.test.ts +1 -0
- package/src/v4/classic/tests/instance-footprint.test.ts +22 -0
- package/src/v4/classic/tests/number.test.ts +39 -0
- package/src/v4/classic/tests/string.test.ts +70 -1
- package/src/v4/classic/tests/to-json-schema.test.ts +87 -35
- package/src/v4/core/api.ts +17 -22
- package/src/v4/core/compile.ts +27 -6
- package/src/v4/core/core.ts +1 -3
- package/src/v4/core/json-schema-processors.ts +9 -13
- package/src/v4/core/regexes.ts +4 -0
- package/src/v4/core/schemas.ts +35 -11
- package/src/v4/core/tests/compile-differential.test.ts +35 -0
- package/src/v4/core/tests/compile.test.ts +177 -0
- package/src/v4/core/tests/url-no-canparse.test.ts +118 -16
- package/src/v4/core/versions.ts +1 -1
- package/src/v4/locales/ar.ts +1 -0
- package/src/v4/locales/az.ts +1 -0
- package/src/v4/locales/be.ts +1 -0
- package/src/v4/locales/bg.ts +1 -0
- package/src/v4/locales/bn.ts +1 -0
- package/src/v4/locales/ca.ts +1 -0
- package/src/v4/locales/ckb.ts +1 -0
- package/src/v4/locales/cs.ts +1 -0
- package/src/v4/locales/da.ts +1 -0
- package/src/v4/locales/de.ts +1 -0
- package/src/v4/locales/el.ts +1 -0
- package/src/v4/locales/en.ts +1 -0
- package/src/v4/locales/eo.ts +1 -0
- package/src/v4/locales/es.ts +1 -0
- package/src/v4/locales/fa.ts +1 -0
- package/src/v4/locales/fi.ts +1 -0
- package/src/v4/locales/fr-CA.ts +1 -0
- package/src/v4/locales/fr.ts +1 -0
- package/src/v4/locales/gu.ts +1 -0
- package/src/v4/locales/he.ts +1 -0
- package/src/v4/locales/hi.ts +1 -0
- package/src/v4/locales/hr.ts +1 -0
- package/src/v4/locales/hu.ts +1 -0
- package/src/v4/locales/hy.ts +1 -0
- package/src/v4/locales/id.ts +1 -0
- package/src/v4/locales/is.ts +1 -0
- package/src/v4/locales/it.ts +1 -0
- package/src/v4/locales/ja.ts +1 -0
- package/src/v4/locales/ka.ts +1 -0
- package/src/v4/locales/km.ts +1 -0
- package/src/v4/locales/kn.ts +1 -0
- package/src/v4/locales/ko.ts +1 -0
- package/src/v4/locales/lt.ts +1 -0
- package/src/v4/locales/mk.ts +1 -0
- package/src/v4/locales/ms.ts +1 -0
- package/src/v4/locales/ne.ts +1 -0
- package/src/v4/locales/nl.ts +1 -0
- package/src/v4/locales/nn.ts +1 -0
- package/src/v4/locales/no.ts +1 -0
- package/src/v4/locales/ota.ts +1 -0
- package/src/v4/locales/pl.ts +1 -0
- package/src/v4/locales/ps.ts +1 -0
- package/src/v4/locales/pt-BR.ts +1 -0
- package/src/v4/locales/pt.ts +1 -0
- package/src/v4/locales/ro.ts +1 -0
- package/src/v4/locales/ru.ts +1 -0
- package/src/v4/locales/sk.ts +1 -0
- package/src/v4/locales/sl.ts +1 -0
- package/src/v4/locales/sv.ts +1 -0
- package/src/v4/locales/ta.ts +1 -0
- package/src/v4/locales/tg.ts +1 -0
- package/src/v4/locales/th.ts +1 -0
- package/src/v4/locales/tk.ts +1 -0
- package/src/v4/locales/tr.ts +1 -0
- package/src/v4/locales/uk.ts +1 -0
- package/src/v4/locales/ur.ts +1 -0
- package/src/v4/locales/uz.ts +1 -0
- package/src/v4/locales/vi.ts +1 -0
- package/src/v4/locales/yo.ts +1 -0
- package/src/v4/locales/zh-CN.ts +1 -0
- package/src/v4/locales/zh-TW.ts +1 -0
- package/src/v4/mini/schemas.ts +12 -4
- package/src/v4/mini/tests/index.test.ts +56 -0
- package/src/v4/mini/tests/string.test.ts +7 -0
- package/v4/classic/schemas.cjs +9 -4
- package/v4/classic/schemas.d.cts +1 -0
- package/v4/classic/schemas.d.ts +1 -0
- package/v4/classic/schemas.js +8 -4
- package/v4/core/api.cjs +9 -20
- package/v4/core/api.d.cts +6 -2
- package/v4/core/api.d.ts +6 -2
- package/v4/core/api.js +9 -20
- package/v4/core/compile.cjs +7 -6
- package/v4/core/compile.js +8 -7
- package/v4/core/core.cjs +1 -1
- package/v4/core/core.js +1 -1
- package/v4/core/json-schema-processors.cjs +7 -10
- package/v4/core/json-schema-processors.js +7 -10
- package/v4/core/regexes.cjs +4 -2
- package/v4/core/regexes.d.cts +1 -0
- package/v4/core/regexes.d.ts +1 -0
- package/v4/core/regexes.js +2 -0
- package/v4/core/schemas.cjs +32 -11
- package/v4/core/schemas.d.cts +5 -3
- package/v4/core/schemas.d.ts +5 -3
- package/v4/core/schemas.js +30 -11
- package/v4/core/versions.cjs +1 -1
- package/v4/core/versions.js +1 -1
- package/v4/locales/ar.cjs +1 -0
- package/v4/locales/ar.js +1 -0
- package/v4/locales/az.cjs +1 -0
- package/v4/locales/az.js +1 -0
- package/v4/locales/be.cjs +1 -0
- package/v4/locales/be.js +1 -0
- package/v4/locales/bg.cjs +1 -0
- package/v4/locales/bg.js +1 -0
- package/v4/locales/bn.cjs +1 -0
- package/v4/locales/bn.js +1 -0
- package/v4/locales/ca.cjs +1 -0
- package/v4/locales/ca.js +1 -0
- package/v4/locales/ckb.cjs +1 -0
- package/v4/locales/ckb.js +1 -0
- package/v4/locales/cs.cjs +1 -0
- package/v4/locales/cs.js +1 -0
- package/v4/locales/da.cjs +1 -0
- package/v4/locales/da.js +1 -0
- package/v4/locales/de.cjs +1 -0
- package/v4/locales/de.js +1 -0
- package/v4/locales/el.cjs +1 -0
- package/v4/locales/el.js +1 -0
- package/v4/locales/en.cjs +1 -0
- package/v4/locales/en.js +1 -0
- package/v4/locales/eo.cjs +1 -0
- package/v4/locales/eo.js +1 -0
- package/v4/locales/es.cjs +1 -0
- package/v4/locales/es.js +1 -0
- package/v4/locales/fa.cjs +1 -0
- package/v4/locales/fa.js +1 -0
- package/v4/locales/fi.cjs +1 -0
- package/v4/locales/fi.js +1 -0
- package/v4/locales/fr-CA.cjs +1 -0
- package/v4/locales/fr-CA.js +1 -0
- package/v4/locales/fr.cjs +1 -0
- package/v4/locales/fr.js +1 -0
- package/v4/locales/gu.cjs +1 -0
- package/v4/locales/gu.js +1 -0
- package/v4/locales/he.cjs +1 -0
- package/v4/locales/he.js +1 -0
- package/v4/locales/hi.cjs +1 -0
- package/v4/locales/hi.js +1 -0
- package/v4/locales/hr.cjs +1 -0
- package/v4/locales/hr.js +1 -0
- package/v4/locales/hu.cjs +1 -0
- package/v4/locales/hu.js +1 -0
- package/v4/locales/hy.cjs +1 -0
- package/v4/locales/hy.js +1 -0
- package/v4/locales/id.cjs +1 -0
- package/v4/locales/id.js +1 -0
- package/v4/locales/is.cjs +1 -0
- package/v4/locales/is.js +1 -0
- package/v4/locales/it.cjs +1 -0
- package/v4/locales/it.js +1 -0
- package/v4/locales/ja.cjs +1 -0
- package/v4/locales/ja.js +1 -0
- package/v4/locales/ka.cjs +1 -0
- package/v4/locales/ka.js +1 -0
- package/v4/locales/km.cjs +1 -0
- package/v4/locales/km.js +1 -0
- package/v4/locales/kn.cjs +1 -0
- package/v4/locales/kn.js +1 -0
- package/v4/locales/ko.cjs +1 -0
- package/v4/locales/ko.js +1 -0
- package/v4/locales/lt.cjs +1 -0
- package/v4/locales/lt.js +1 -0
- package/v4/locales/mk.cjs +1 -0
- package/v4/locales/mk.js +1 -0
- package/v4/locales/ms.cjs +1 -0
- package/v4/locales/ms.js +1 -0
- package/v4/locales/ne.cjs +1 -0
- package/v4/locales/ne.js +1 -0
- package/v4/locales/nl.cjs +1 -0
- package/v4/locales/nl.js +1 -0
- package/v4/locales/nn.cjs +1 -0
- package/v4/locales/nn.js +1 -0
- package/v4/locales/no.cjs +1 -0
- package/v4/locales/no.js +1 -0
- package/v4/locales/ota.cjs +1 -0
- package/v4/locales/ota.js +1 -0
- package/v4/locales/pl.cjs +1 -0
- package/v4/locales/pl.js +1 -0
- package/v4/locales/ps.cjs +1 -0
- package/v4/locales/ps.js +1 -0
- package/v4/locales/pt-BR.cjs +1 -0
- package/v4/locales/pt-BR.js +1 -0
- package/v4/locales/pt.cjs +1 -0
- package/v4/locales/pt.js +1 -0
- package/v4/locales/ro.cjs +1 -0
- package/v4/locales/ro.js +1 -0
- package/v4/locales/ru.cjs +1 -0
- package/v4/locales/ru.js +1 -0
- package/v4/locales/sk.cjs +1 -0
- package/v4/locales/sk.js +1 -0
- package/v4/locales/sl.cjs +1 -0
- package/v4/locales/sl.js +1 -0
- package/v4/locales/sv.cjs +1 -0
- package/v4/locales/sv.js +1 -0
- package/v4/locales/ta.cjs +1 -0
- package/v4/locales/ta.js +1 -0
- package/v4/locales/tg.cjs +1 -0
- package/v4/locales/tg.js +1 -0
- package/v4/locales/th.cjs +1 -0
- package/v4/locales/th.js +1 -0
- package/v4/locales/tk.cjs +1 -0
- package/v4/locales/tk.js +1 -0
- package/v4/locales/tr.cjs +1 -0
- package/v4/locales/tr.js +1 -0
- package/v4/locales/uk.cjs +1 -0
- package/v4/locales/uk.js +1 -0
- package/v4/locales/ur.cjs +1 -0
- package/v4/locales/ur.js +1 -0
- package/v4/locales/uz.cjs +1 -0
- package/v4/locales/uz.js +1 -0
- package/v4/locales/vi.cjs +1 -0
- package/v4/locales/vi.js +1 -0
- package/v4/locales/yo.cjs +1 -0
- package/v4/locales/yo.js +1 -0
- package/v4/locales/zh-CN.cjs +1 -0
- package/v4/locales/zh-CN.js +1 -0
- package/v4/locales/zh-TW.cjs +1 -0
- package/v4/locales/zh-TW.js +1 -0
- package/v4/mini/schemas.cjs +10 -4
- package/v4/mini/schemas.d.cts +1 -0
- package/v4/mini/schemas.d.ts +1 -0
- package/v4/mini/schemas.js +9 -4
package/src/v4/core/schemas.ts
CHANGED
|
@@ -522,10 +522,30 @@ export interface $ZodURL extends $ZodType {
|
|
|
522
522
|
|
|
523
523
|
/** The `://` guard rejected the input before the URL constructor saw it. */
|
|
524
524
|
export const URL_BAD_FORMAT = 1;
|
|
525
|
-
/** The URL
|
|
525
|
+
/** The URL parser rejected the input. */
|
|
526
526
|
export const URL_UNPARSEABLE = 2;
|
|
527
527
|
|
|
528
|
-
|
|
528
|
+
export function canParseURL(input: string): boolean {
|
|
529
|
+
try {
|
|
530
|
+
if (typeof URL !== "undefined" && typeof URL.canParse === "function") return URL.canParse(input);
|
|
531
|
+
new URL(input);
|
|
532
|
+
return true;
|
|
533
|
+
} catch {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function validateURL(
|
|
539
|
+
trimmed: string,
|
|
540
|
+
def: Pick<$ZodURLDef, "protocol" | "hostname" | "normalize">
|
|
541
|
+
): URL | true | typeof URL_BAD_FORMAT | typeof URL_UNPARSEABLE {
|
|
542
|
+
if (!("normalize" in def) && !("hostname" in def) && !("protocol" in def)) {
|
|
543
|
+
return canParseURL(trimmed) || URL_UNPARSEABLE;
|
|
544
|
+
}
|
|
545
|
+
return parseURLObject(trimmed, def);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** Parses a URL while preserving the non-normalizing HTTP guard. */
|
|
529
549
|
export function parseURLObject(
|
|
530
550
|
trimmed: string,
|
|
531
551
|
def: Pick<$ZodURLDef, "protocol" | "normalize">
|
|
@@ -536,6 +556,10 @@ export function parseURLObject(
|
|
|
536
556
|
}
|
|
537
557
|
|
|
538
558
|
try {
|
|
559
|
+
if (typeof URL !== "undefined") {
|
|
560
|
+
const URLStatic = URL as typeof URL & { parse?: (input: string) => URL | null };
|
|
561
|
+
if (typeof URLStatic.parse === "function") return URLStatic.parse(trimmed) ?? URL_UNPARSEABLE;
|
|
562
|
+
}
|
|
539
563
|
// @ts-ignore
|
|
540
564
|
return new URL(trimmed);
|
|
541
565
|
} catch {
|
|
@@ -566,7 +590,7 @@ export const $ZodURL: core.$constructor<$ZodURL> = /*@__PURE__*/ core.$construct
|
|
|
566
590
|
try {
|
|
567
591
|
// Trim whitespace from input
|
|
568
592
|
const trimmed = payload.value.trim();
|
|
569
|
-
const url =
|
|
593
|
+
const url = validateURL(trimmed, def);
|
|
570
594
|
|
|
571
595
|
if (url === URL_BAD_FORMAT) {
|
|
572
596
|
payload.issues.push({
|
|
@@ -591,6 +615,11 @@ export const $ZodURL: core.$constructor<$ZodURL> = /*@__PURE__*/ core.$construct
|
|
|
591
615
|
return;
|
|
592
616
|
}
|
|
593
617
|
|
|
618
|
+
if (url === true) {
|
|
619
|
+
payload.value = stripTabAndNewline(trimmed);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
594
623
|
if (def.hostname && !urlHostnameOk(url, def.hostname)) {
|
|
595
624
|
payload.issues.push({
|
|
596
625
|
code: "invalid_format",
|
|
@@ -885,13 +914,7 @@ const ipv6Alphabet = /^[0-9a-fA-F:.]+$/;
|
|
|
885
914
|
|
|
886
915
|
export function isValidIPv6(value: string): boolean {
|
|
887
916
|
if (!ipv6Alphabet.test(value)) return false;
|
|
888
|
-
|
|
889
|
-
// @ts-ignore
|
|
890
|
-
new URL(`http://[${value}]`);
|
|
891
|
-
return true;
|
|
892
|
-
} catch {
|
|
893
|
-
return false;
|
|
894
|
-
}
|
|
917
|
+
return canParseURL(`http://[${value}]`);
|
|
895
918
|
}
|
|
896
919
|
|
|
897
920
|
export const $ZodIPv6: core.$constructor<$ZodIPv6> = /*@__PURE__*/ core.$constructor("$ZodIPv6", (inst, def): void => {
|
|
@@ -5165,4 +5188,5 @@ export type $ZodStringFormatTypes =
|
|
|
5165
5188
|
| $ZodJWT
|
|
5166
5189
|
| $ZodCustomStringFormat<"hex">
|
|
5167
5190
|
| $ZodCustomStringFormat<util.HashFormat>
|
|
5168
|
-
| $ZodCustomStringFormat<"hostname"
|
|
5191
|
+
| $ZodCustomStringFormat<"hostname">
|
|
5192
|
+
| $ZodCustomStringFormat<"currency_code">;
|
|
@@ -677,6 +677,41 @@ test("url options match the runtime across the whole option matrix", () => {
|
|
|
677
677
|
for (const schema of schemas) differential(schema, inputs);
|
|
678
678
|
});
|
|
679
679
|
|
|
680
|
+
test("assert-only URL generation skips unused output without skipping chained transforms", () => {
|
|
681
|
+
const sanitized = "https://example.com";
|
|
682
|
+
const withNewline = "https://exa\nmple.com";
|
|
683
|
+
differential(z.url().length(sanitized.length), [sanitized, withNewline, "https://example.com/long"]);
|
|
684
|
+
differential(z.string().url().length(sanitized.length), [sanitized, withNewline, "https://example.com/long"]);
|
|
685
|
+
|
|
686
|
+
const URLStatic = URL as typeof URL & { parse: (input: string) => URL | null };
|
|
687
|
+
const descriptor = Object.getOwnPropertyDescriptor(URLStatic, "parse")!;
|
|
688
|
+
const marker = new Error("href read");
|
|
689
|
+
Object.defineProperty(URLStatic, "parse", {
|
|
690
|
+
configurable: true,
|
|
691
|
+
value: () =>
|
|
692
|
+
Object.create(URL.prototype, {
|
|
693
|
+
href: {
|
|
694
|
+
get() {
|
|
695
|
+
throw marker;
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
}) as URL,
|
|
699
|
+
});
|
|
700
|
+
try {
|
|
701
|
+
const schema = z.url({ normalize: true });
|
|
702
|
+
const parser = compileFn(schema);
|
|
703
|
+
const validator = compileFn(schema, { assertOnly: true });
|
|
704
|
+
expect(validator("https://example.com")).toBe(true);
|
|
705
|
+
expect(z.validate(compile(z.url({ normalize: true })), "https://example.com")).toBe(true);
|
|
706
|
+
expect(z.validate(compile(z.object({ url: z.url({ normalize: true }) })), { url: "https://example.com" })).toBe(
|
|
707
|
+
true
|
|
708
|
+
);
|
|
709
|
+
expect(() => parser("https://example.com")).toThrow(marker);
|
|
710
|
+
} finally {
|
|
711
|
+
Object.defineProperty(URLStatic, "parse", descriptor);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
|
|
680
715
|
test("empty strict object compiles", () => {
|
|
681
716
|
// With no keys the unknown-key condition is empty, and `if () return INVALID;` is a syntax error the single top-level `new Function` rejects — too late for compileChild to island, so the whole tree lost its fast path.
|
|
682
717
|
differential(z.strictObject({}), [{}, { a: 1 }, Object.create({ inherited: 1 })]);
|
|
@@ -1787,6 +1787,183 @@ test("compiled records walk own enumerable keys without Reflect.ownKeys", () =>
|
|
|
1787
1787
|
expect(compile(plain).parse(revealing())).toStrictEqual({ a: 1, b: 2 });
|
|
1788
1788
|
});
|
|
1789
1789
|
|
|
1790
|
+
test("validate honors custom checks composed with format traits", () => {
|
|
1791
|
+
const postProcessor = z.core.globalConfig.postProcessor;
|
|
1792
|
+
z.core.globalConfig.postProcessor = undefined;
|
|
1793
|
+
try {
|
|
1794
|
+
const CustomEmail = z.core.$constructor<z.core.$ZodEmail>("CustomEmail", (inst, def) => {
|
|
1795
|
+
inst._zod.check = (payload) => {
|
|
1796
|
+
if (payload.value !== "custom") payload.issues.push({ code: "custom", input: payload.value });
|
|
1797
|
+
};
|
|
1798
|
+
z.core.$ZodEmail.init(inst, def);
|
|
1799
|
+
});
|
|
1800
|
+
const schema = new CustomEmail({ type: "string", format: "email", check: "string_format" });
|
|
1801
|
+
for (const [input, valid] of [
|
|
1802
|
+
["custom", true],
|
|
1803
|
+
["test@example.com", false],
|
|
1804
|
+
[42, false],
|
|
1805
|
+
] as const) {
|
|
1806
|
+
expect(z.safeParse(schema, input).success).toBe(valid);
|
|
1807
|
+
expect(z.validate(schema, input)).toBe(valid);
|
|
1808
|
+
}
|
|
1809
|
+
} finally {
|
|
1810
|
+
z.core.globalConfig.postProcessor = postProcessor;
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
test("validate honors standalone format checks", () => {
|
|
1815
|
+
const cases = [
|
|
1816
|
+
[z.iso.datetime(), "2021-01-01T00:00:00Z"],
|
|
1817
|
+
[z.iso.date(), "2021-01-01"],
|
|
1818
|
+
[z.iso.time(), "00:00:00"],
|
|
1819
|
+
[z.iso.duration(), "P1Y2M3DT4H5M6S"],
|
|
1820
|
+
[z.email(), "test@example.com"],
|
|
1821
|
+
[z.uuid(), "20354d7a-e4fe-47af-8ff6-187bca92f3f9"],
|
|
1822
|
+
[z.ipv4(), "192.168.0.1"],
|
|
1823
|
+
] as const;
|
|
1824
|
+
|
|
1825
|
+
for (const [schema, valid] of cases) {
|
|
1826
|
+
expect(schema._zod.traits.has("$ZodStringFormat")).toBe(true);
|
|
1827
|
+
expect(schema instanceof z.core.$ZodStringFormat).toBe(true);
|
|
1828
|
+
expect(z.validate(schema, valid)).toBe(true);
|
|
1829
|
+
expect(z.validate(schema, "invalid")).toBe(false);
|
|
1830
|
+
expect(z.validate(schema, 1)).toBe(false);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
const source = z.email();
|
|
1834
|
+
const refined = source.refine((value) => value.startsWith("x"));
|
|
1835
|
+
const asyncRefined = source.refine(async () => true);
|
|
1836
|
+
const coercedDef = Object.assign(Object.create({ coerce: true }), source._zod.def);
|
|
1837
|
+
const coerced = source.clone(coercedDef);
|
|
1838
|
+
const checkedDef = Object.assign(Object.create({ checks: refined._zod.def.checks }), source._zod.def);
|
|
1839
|
+
const inheritedChecked = source.clone(checkedDef);
|
|
1840
|
+
expect(z.validate(refined, "test@example.com")).toBe(false);
|
|
1841
|
+
expect(z.validate(refined, "x@example.com")).toBe(true);
|
|
1842
|
+
expect(z.validate(inheritedChecked, "test@example.com")).toBe(false);
|
|
1843
|
+
expect(() => z.validate(asyncRefined, "test@example.com")).toThrow($ZodAsyncError);
|
|
1844
|
+
expect(z.validate(coerced, { toString: () => "test@example.com" })).toBe(true);
|
|
1845
|
+
});
|
|
1846
|
+
|
|
1847
|
+
test("runtime validate preserves contexts, getters, errors, and regex state", () => {
|
|
1848
|
+
const postProcessor = z.core.globalConfig.postProcessor;
|
|
1849
|
+
z.core.globalConfig.postProcessor = undefined;
|
|
1850
|
+
try {
|
|
1851
|
+
const source = z.email();
|
|
1852
|
+
expect(z.validate(source, "invalid", { skipChecks: true } as any)).toBe(true);
|
|
1853
|
+
|
|
1854
|
+
let whenReads = 0;
|
|
1855
|
+
const whenProto = Object.defineProperty({}, "when", {
|
|
1856
|
+
enumerable: true,
|
|
1857
|
+
get() {
|
|
1858
|
+
whenReads++;
|
|
1859
|
+
return () => false;
|
|
1860
|
+
},
|
|
1861
|
+
});
|
|
1862
|
+
const whenDef = Object.assign(Object.create(whenProto), source._zod.def);
|
|
1863
|
+
const gated = source.clone(whenDef as typeof source._zod.def);
|
|
1864
|
+
expect(whenReads).toBe(0);
|
|
1865
|
+
expect(z.validate(gated, "invalid")).toBe(true);
|
|
1866
|
+
expect(whenReads).toBeGreaterThan(0);
|
|
1867
|
+
|
|
1868
|
+
let errorReads = 0;
|
|
1869
|
+
const errorDef = Object.defineProperties({}, Object.getOwnPropertyDescriptors(source._zod.def));
|
|
1870
|
+
Object.defineProperty(errorDef, "error", {
|
|
1871
|
+
enumerable: true,
|
|
1872
|
+
get() {
|
|
1873
|
+
errorReads++;
|
|
1874
|
+
return () => "custom";
|
|
1875
|
+
},
|
|
1876
|
+
});
|
|
1877
|
+
const customError = source.clone(errorDef as typeof source._zod.def);
|
|
1878
|
+
expect(z.validate(customError, "invalid")).toBe(false);
|
|
1879
|
+
expect(errorReads).toBe(0);
|
|
1880
|
+
const result = customError.safeParse("invalid");
|
|
1881
|
+
expect(errorReads).toBe(0);
|
|
1882
|
+
if (result.success) expect.unreachable();
|
|
1883
|
+
expect(result.error.issues[0]?.message).toBe("custom");
|
|
1884
|
+
expect(errorReads).toBeGreaterThan(0);
|
|
1885
|
+
|
|
1886
|
+
const pattern = /^a$/g;
|
|
1887
|
+
const cloned = source.clone({ ...source._zod.def, pattern });
|
|
1888
|
+
for (let i = 0; i < 3; i++) expect(z.validate(cloned, "a")).toBe(true);
|
|
1889
|
+
expect(z.validate(cloned, "b")).toBe(false);
|
|
1890
|
+
expect(pattern.lastIndex).toBe(0);
|
|
1891
|
+
const replacement = /^b$/g;
|
|
1892
|
+
cloned._zod.def.pattern = replacement;
|
|
1893
|
+
expect(z.validate(cloned, "a")).toBe(false);
|
|
1894
|
+
expect(z.validate(cloned, "b")).toBe(true);
|
|
1895
|
+
} finally {
|
|
1896
|
+
z.core.globalConfig.postProcessor = postProcessor;
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
|
|
1900
|
+
test("validate preserves live pattern accessor reads", () => {
|
|
1901
|
+
const postProcessor = z.core.globalConfig.postProcessor;
|
|
1902
|
+
z.core.globalConfig.postProcessor = undefined;
|
|
1903
|
+
try {
|
|
1904
|
+
const schema = z.email();
|
|
1905
|
+
let reads = 0;
|
|
1906
|
+
Object.defineProperty(schema.def, "pattern", {
|
|
1907
|
+
get: () => (++reads % 2 === 1 ? /^a$/ : /^b$/),
|
|
1908
|
+
});
|
|
1909
|
+
expect(schema.safeParse("b").success).toBe(true);
|
|
1910
|
+
expect(reads).toBe(2);
|
|
1911
|
+
reads = 0;
|
|
1912
|
+
expect(z.validate(schema, "b")).toBe(true);
|
|
1913
|
+
expect(reads).toBe(2);
|
|
1914
|
+
} finally {
|
|
1915
|
+
z.core.globalConfig.postProcessor = postProcessor;
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
test.each(["when", "coerce"] as const)("runtime validate honors live %s options", (option) => {
|
|
1920
|
+
const postProcessor = z.core.globalConfig.postProcessor;
|
|
1921
|
+
z.core.globalConfig.postProcessor = undefined;
|
|
1922
|
+
try {
|
|
1923
|
+
for (const inherited of [false, true]) {
|
|
1924
|
+
const schema = z.email();
|
|
1925
|
+
const input = option === "when" ? "invalid" : { toString: () => "test@example.com" };
|
|
1926
|
+
expect(z.validate(schema, input)).toBe(false);
|
|
1927
|
+
|
|
1928
|
+
const target = inherited ? Object.create(Object.getPrototypeOf(schema.def)) : schema.def;
|
|
1929
|
+
let reads = 0;
|
|
1930
|
+
Object.defineProperty(target, option, {
|
|
1931
|
+
get() {
|
|
1932
|
+
reads++;
|
|
1933
|
+
return option === "when" ? () => false : true;
|
|
1934
|
+
},
|
|
1935
|
+
});
|
|
1936
|
+
if (inherited) Object.setPrototypeOf(schema.def, target);
|
|
1937
|
+
expect(reads).toBe(0);
|
|
1938
|
+
expect(z.validate(schema, input)).toBe(true);
|
|
1939
|
+
expect(reads).toBeGreaterThan(0);
|
|
1940
|
+
expect(schema.safeParse(input).success).toBe(true);
|
|
1941
|
+
}
|
|
1942
|
+
} finally {
|
|
1943
|
+
z.core.globalConfig.postProcessor = postProcessor;
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
test("validate uses compiled validators and runtime fallbacks", () => {
|
|
1948
|
+
const compiled = compile(z.email());
|
|
1949
|
+
expect((compiled._zod.bag as { validator?: unknown }).validator).toBeTypeOf("function");
|
|
1950
|
+
expect(z.validate(compiled, "test@example.com")).toBe(true);
|
|
1951
|
+
expect(z.validate(compiled, "invalid")).toBe(false);
|
|
1952
|
+
expect(z.validate(compiled, "invalid", { skipChecks: true } as any)).toBe(true);
|
|
1953
|
+
|
|
1954
|
+
const source = z.email();
|
|
1955
|
+
const installed = withParser(source, () => INVALID);
|
|
1956
|
+
const bag = installed._zod.bag as { fallbackRun: typeof source._zod.run };
|
|
1957
|
+
const originalRun = bag.fallbackRun;
|
|
1958
|
+
let fallbackRuns = 0;
|
|
1959
|
+
bag.fallbackRun = (payload, ctx) => {
|
|
1960
|
+
fallbackRuns++;
|
|
1961
|
+
return originalRun(payload, ctx);
|
|
1962
|
+
};
|
|
1963
|
+
expect(z.validate(installed, "invalid")).toBe(false);
|
|
1964
|
+
expect(fallbackRuns).toBeGreaterThan(0);
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1790
1967
|
// withParser: a parser Zod did not generate, installed on the same wrapper compile() uses.
|
|
1791
1968
|
|
|
1792
1969
|
// tags its output so a test fails loudly if the runtime ran instead of the supplied parser
|
|
@@ -1,31 +1,133 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const URLDescriptor = Object.getOwnPropertyDescriptor(globalThis, "URL")!;
|
|
4
|
+
const NativeURL = globalThis.URL;
|
|
5
|
+
const canParseDescriptor = Object.getOwnPropertyDescriptor(NativeURL, "canParse")!;
|
|
6
|
+
const parseDescriptor = Object.getOwnPropertyDescriptor(NativeURL, "parse")!;
|
|
6
7
|
|
|
8
|
+
function restoreURL() {
|
|
9
|
+
Object.defineProperty(globalThis, "URL", URLDescriptor);
|
|
10
|
+
Object.defineProperty(NativeURL, "canParse", canParseDescriptor);
|
|
11
|
+
Object.defineProperty(NativeURL, "parse", parseDescriptor);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("URL parser fallbacks", () => {
|
|
7
15
|
beforeEach(() => {
|
|
16
|
+
restoreURL();
|
|
8
17
|
vi.resetModules();
|
|
9
|
-
// @ts-expect-error force the fallback branch (URL.canParse may be undefined on older runtimes)
|
|
10
|
-
delete URL.canParse;
|
|
11
18
|
});
|
|
12
19
|
|
|
13
|
-
afterEach(
|
|
14
|
-
|
|
20
|
+
afterEach(restoreURL);
|
|
21
|
+
|
|
22
|
+
test("uses the boolean and object APIs on modern runtimes", async () => {
|
|
23
|
+
const canParse = vi.spyOn(NativeURL, "canParse");
|
|
24
|
+
const URLStatic = NativeURL as typeof URL & { parse: (input: string) => URL | null };
|
|
25
|
+
const parse = vi.spyOn(URLStatic, "parse");
|
|
26
|
+
const z = await import("../../index.js");
|
|
27
|
+
|
|
28
|
+
expect(z.validate(z.url(), "https://example.com")).toBe(true);
|
|
29
|
+
expect(canParse).toHaveBeenCalledOnce();
|
|
30
|
+
expect(parse).not.toHaveBeenCalled();
|
|
31
|
+
|
|
32
|
+
canParse.mockClear();
|
|
33
|
+
expect(z.validate(z.url({ normalize: true }), "https://example.com")).toBe(true);
|
|
34
|
+
expect(parse).toHaveBeenCalledOnce();
|
|
35
|
+
expect(canParse).not.toHaveBeenCalled();
|
|
36
|
+
|
|
37
|
+
parse.mockClear();
|
|
38
|
+
expect(z.validate(z.ipv6(), "2001:db8::1")).toBe(true);
|
|
39
|
+
expect(canParse).toHaveBeenCalledOnce();
|
|
40
|
+
expect(parse).not.toHaveBeenCalled();
|
|
15
41
|
});
|
|
16
42
|
|
|
17
|
-
test("
|
|
18
|
-
expect(URL.canParse).toBeUndefined();
|
|
43
|
+
test("URL fast-path selection preserves live option getter reads", async () => {
|
|
19
44
|
const z = await import("../../index.js");
|
|
20
|
-
|
|
21
|
-
|
|
45
|
+
const postProcessor = z.core.globalConfig.postProcessor;
|
|
46
|
+
z.core.globalConfig.postProcessor = undefined;
|
|
47
|
+
try {
|
|
48
|
+
for (const inherited of [false, true]) {
|
|
49
|
+
const schema = z.url();
|
|
50
|
+
const target = inherited ? Object.create(Object.getPrototypeOf(schema.def)) : schema.def;
|
|
51
|
+
let reads = 0;
|
|
52
|
+
Object.defineProperty(target, "hostname", {
|
|
53
|
+
get: () => (++reads === 1 ? /^example\.com$/ : undefined),
|
|
54
|
+
});
|
|
55
|
+
if (inherited) Object.setPrototypeOf(schema.def, target);
|
|
56
|
+
expect(z.validate(schema, "https://example.com")).toBe(false);
|
|
57
|
+
expect(reads).toBe(2);
|
|
58
|
+
reads = 0;
|
|
59
|
+
expect(schema.safeParse("https://example.com").success).toBe(false);
|
|
60
|
+
expect(reads).toBe(2);
|
|
61
|
+
}
|
|
62
|
+
} finally {
|
|
63
|
+
z.core.globalConfig.postProcessor = postProcessor;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("the exported object parser still returns a URL", async () => {
|
|
68
|
+
const { parseURLObject } = await import("../schemas.js");
|
|
69
|
+
const result = parseURLObject("https://example.com", {});
|
|
70
|
+
expect(result).toBeInstanceOf(NativeURL);
|
|
71
|
+
if (typeof result === "number") expect.unreachable();
|
|
72
|
+
expect(result.hostname).toBe("example.com");
|
|
73
|
+
expect(parseURLObject("invalid", {})).toBe(2);
|
|
22
74
|
});
|
|
23
75
|
|
|
24
|
-
test("
|
|
25
|
-
|
|
76
|
+
test("falls back when URL.canParse is absent", async () => {
|
|
77
|
+
Reflect.deleteProperty(NativeURL, "canParse");
|
|
26
78
|
const z = await import("../../index.js");
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
79
|
+
const cases = [
|
|
80
|
+
[z.url(), "https://example.com", "not a url"],
|
|
81
|
+
[z.ipv6(), "2001:db8::1", "not-an-ip"],
|
|
82
|
+
[z.cidrv6(), "2001:db8::/32", "not-an-ip/32"],
|
|
83
|
+
] as const;
|
|
84
|
+
|
|
85
|
+
for (const [schema, valid, invalid] of cases) {
|
|
86
|
+
for (const subject of [schema, z.compile(schema)]) {
|
|
87
|
+
expect(z.validate(subject, valid)).toBe(true);
|
|
88
|
+
expect(z.validate(subject, invalid)).toBe(false);
|
|
89
|
+
expect(subject.safeParse(valid).success).toBe(true);
|
|
90
|
+
expect(subject.safeParse(invalid).success).toBe(false);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("falls back when URL.parse is absent", async () => {
|
|
96
|
+
Reflect.deleteProperty(NativeURL, "parse");
|
|
97
|
+
const z = await import("../../index.js");
|
|
98
|
+
const cases = [
|
|
99
|
+
[z.url({ normalize: true }), "https://example.com", "not a url"],
|
|
100
|
+
[z.url({ hostname: /^example\.com$/ }), "https://example.com", "https://other.com"],
|
|
101
|
+
[z.httpUrl(), "https://example.com", "mailto:a@example.com"],
|
|
102
|
+
] as const;
|
|
103
|
+
|
|
104
|
+
for (const [schema, valid, invalid] of cases) {
|
|
105
|
+
for (const subject of [schema, z.compile(schema)]) {
|
|
106
|
+
expect(z.validate(subject, valid)).toBe(true);
|
|
107
|
+
expect(z.validate(subject, invalid)).toBe(false);
|
|
108
|
+
expect(subject.safeParse(valid).success).toBe(true);
|
|
109
|
+
expect(subject.safeParse(invalid).success).toBe(false);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("rejects without a global URL constructor", async () => {
|
|
115
|
+
const z = await import("../../index.js");
|
|
116
|
+
const cases = [
|
|
117
|
+
[z.url(), "https://example.com"],
|
|
118
|
+
[z.url({ normalize: true }), "https://example.com"],
|
|
119
|
+
[z.ipv6(), "2001:db8::1"],
|
|
120
|
+
[z.cidrv6(), "2001:db8::/32"],
|
|
121
|
+
] as const;
|
|
122
|
+
const subjects = cases.flatMap(([schema, input]) => [
|
|
123
|
+
[schema, input] as const,
|
|
124
|
+
[z.compile(schema), input] as const,
|
|
125
|
+
]);
|
|
126
|
+
Object.defineProperty(globalThis, "URL", { configurable: true, value: undefined });
|
|
127
|
+
|
|
128
|
+
for (const [schema, input] of subjects) {
|
|
129
|
+
expect(z.validate(schema, input)).toBe(false);
|
|
130
|
+
expect(schema.safeParse(input).success).toBe(false);
|
|
131
|
+
}
|
|
30
132
|
});
|
|
31
133
|
});
|
package/src/v4/core/versions.ts
CHANGED
package/src/v4/locales/ar.ts
CHANGED
package/src/v4/locales/az.ts
CHANGED
package/src/v4/locales/be.ts
CHANGED
package/src/v4/locales/bg.ts
CHANGED
package/src/v4/locales/bn.ts
CHANGED
package/src/v4/locales/ca.ts
CHANGED
package/src/v4/locales/ckb.ts
CHANGED
package/src/v4/locales/cs.ts
CHANGED
package/src/v4/locales/da.ts
CHANGED
package/src/v4/locales/de.ts
CHANGED
package/src/v4/locales/el.ts
CHANGED
package/src/v4/locales/en.ts
CHANGED
package/src/v4/locales/eo.ts
CHANGED
package/src/v4/locales/es.ts
CHANGED
package/src/v4/locales/fa.ts
CHANGED
package/src/v4/locales/fi.ts
CHANGED
package/src/v4/locales/fr-CA.ts
CHANGED
package/src/v4/locales/fr.ts
CHANGED
|
@@ -45,6 +45,7 @@ const error: () => errors.$ZodErrorMap = () => {
|
|
|
45
45
|
json_string: "chaîne de caractères JSON",
|
|
46
46
|
e164: "numéro au format E.164",
|
|
47
47
|
credit_card: "numéro de carte de crédit",
|
|
48
|
+
currency_code: "code de devise",
|
|
48
49
|
iban: "IBAN",
|
|
49
50
|
jwt: "JWT",
|
|
50
51
|
template_literal: "entrée",
|
package/src/v4/locales/gu.ts
CHANGED
package/src/v4/locales/he.ts
CHANGED
|
@@ -88,6 +88,7 @@ const error: () => errors.$ZodErrorMap = () => {
|
|
|
88
88
|
json_string: { label: "מחרוזת JSON", gender: "f" },
|
|
89
89
|
e164: { label: "מספר E.164", gender: "m" },
|
|
90
90
|
credit_card: { label: "מספר כרטיס אשראי", gender: "m" },
|
|
91
|
+
currency_code: { label: "קוד מטבע", gender: "m" },
|
|
91
92
|
iban: { label: "IBAN", gender: "m" },
|
|
92
93
|
jwt: { label: "JWT", gender: "m" },
|
|
93
94
|
template_literal: { label: "קלט", gender: "m" },
|
package/src/v4/locales/hi.ts
CHANGED