diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts index ca1ca904d4..9807a03964 100644 --- a/packages/zod/src/v4/classic/schemas.ts +++ b/packages/zod/src/v4/classic/schemas.ts @@ -389,6 +389,7 @@ export interface ZodString extends _ZodString> ): this; /** @deprecated Use `z.iso.duration()` instead. */ duration(params?: string | core.$ZodCheckISODurationParams): this; + script(name: string, params?: string | core.$ZodCheckScriptParams): this; } export const ZodString: core.$constructor = /*@__PURE__*/ core.$constructor("ZodString", (inst, def) => { @@ -418,6 +419,7 @@ export const ZodString: core.$constructor = /*@__PURE__*/ core.$const inst.cidrv4 = (params) => inst.check(core._cidrv4(ZodCIDRv4, params)); inst.cidrv6 = (params) => inst.check(core._cidrv6(ZodCIDRv6, params)); inst.e164 = (params) => inst.check(core._e164(ZodE164, params)); + inst.script = (name, params) => inst.check(core._script(ZodScript, name, params)); // iso inst.datetime = (params) => inst.check(iso.datetime(params as any)); @@ -537,6 +539,18 @@ export function emoji(params?: string | core.$ZodEmojiParams): ZodEmoji { return core._emoji(ZodEmoji, params); } +// ZodScript +export interface ZodScript extends ZodStringFormat<"script"> { + _zod: core.$ZodScriptInternals; +} +export const ZodScript: core.$constructor = /*@__PURE__*/ core.$constructor("ZodScript", (inst, def) => { + core.$ZodScript.init(inst, def); + ZodStringFormat.init(inst, def); +}); +export function script(name: string, params?: string | core.$ZodScriptParams): ZodScript { + return core._script(ZodScript, name, params); +} + // ZodNanoID export interface ZodNanoID extends ZodStringFormat<"nanoid"> { _zod: core.$ZodNanoIDInternals; diff --git a/packages/zod/src/v4/classic/tests/string.test.ts b/packages/zod/src/v4/classic/tests/string.test.ts index 4528beb8aa..dea076bb4e 100644 --- a/packages/zod/src/v4/classic/tests/string.test.ts +++ b/packages/zod/src/v4/classic/tests/string.test.ts @@ -507,6 +507,34 @@ test("emoji validations", () => { expect(() => emoji.parse("stuff😀")).toThrow(); }); +test("script validations", () => { + const arabic = z.script("Arabic"); + arabic.parse("مرحبا"); + arabic.parse("عالم"); + arabic.parse("أمين"); + expect(() => arabic.parse("hello")).toThrow(); + expect(() => arabic.parse("مرحبا123")).toThrow(); + + const latin = z.script("Latin"); + latin.parse("hello"); + expect(() => latin.parse("مرحبا")).toThrow(); + + const cyrillic = z.script("Cyrillic"); + cyrillic.parse("привет"); + expect(() => cyrillic.parse("hello")).toThrow(); + + // method form is equivalent to the standalone factory + expect(() => z.string().script("Cyrillic").parse("hello")).toThrow(); + + expect(() => z.script("NotAScript")).toThrow(SyntaxError); + + const result = arabic.safeParse("hello"); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].code).toBe("invalid_format"); + } +}); + test("nanoid", () => { const nanoid = z.string().nanoid("custom error"); nanoid.parse("lfNZluvAxMkf7Q8C5H-QS"); diff --git a/packages/zod/src/v4/core/api.ts b/packages/zod/src/v4/core/api.ts index 6e38c305f9..c0bffa2500 100644 --- a/packages/zod/src/v4/core/api.ts +++ b/packages/zod/src/v4/core/api.ts @@ -228,6 +228,25 @@ export function _emoji( }); } +// Script +export type $ZodScriptParams = StringFormatParams; +export type $ZodCheckScriptParams = CheckStringFormatParams; +// @__NO_SIDE_EFFECTS__ +export function _script( + Class: util.SchemaClass, + name: string, + params?: string | Omit<$ZodScriptParams, "name"> | Omit<$ZodCheckScriptParams, "name"> +): T { + return new Class({ + type: "string", + format: "script", + check: "string_format", + abort: false, + name, + ...util.normalizeParams(params), + }); +} + // NanoID export type $ZodNanoIDParams = StringFormatParams; export type $ZodCheckNanoIDParams = CheckStringFormatParams; diff --git a/packages/zod/src/v4/core/regexes.ts b/packages/zod/src/v4/core/regexes.ts index 0d3e228e86..11d0c852da 100644 --- a/packages/zod/src/v4/core/regexes.ts +++ b/packages/zod/src/v4/core/regexes.ts @@ -183,3 +183,7 @@ export const sha384_base64url: RegExp = /*@__PURE__*/ fixedBase64url(64); export const sha512_hex: RegExp = /^[0-9a-fA-F]{128}$/; export const sha512_base64: RegExp = /*@__PURE__*/ fixedBase64(86, "=="); export const sha512_base64url: RegExp = /*@__PURE__*/ fixedBase64url(86); + +export function script(name: string): RegExp { + return new RegExp(`^\\p{Script=${name}}+$`, "u"); +} diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index 9642d35906..ff3fb30784 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -567,6 +567,25 @@ export const $ZodEmoji: core.$constructor<$ZodEmoji> = /*@__PURE__*/ core.$const } ); +////////////////////////////// ZodScript ////////////////////////////// + +export interface $ZodScriptDef extends $ZodStringFormatDef<"script"> { + name: string; +} +export interface $ZodScriptInternals extends $ZodStringFormatInternals<"script"> { + def: $ZodScriptDef; +} +export interface $ZodScript extends $ZodType { + _zod: $ZodScriptInternals; +} +export const $ZodScript: core.$constructor<$ZodScript> = /*@__PURE__*/ core.$constructor( + "$ZodScript", + (inst, def): void => { + def.pattern ??= regexes.script(def.name); + $ZodStringFormat.init(inst, def); + } +); + ////////////////////////////// ZodNanoID ////////////////////////////// export interface $ZodNanoIDDef extends $ZodStringFormatDef<"nanoid"> {} @@ -4566,4 +4585,5 @@ export type $ZodStringFormatTypes = | $ZodJWT | $ZodCustomStringFormat<"hex"> | $ZodCustomStringFormat - | $ZodCustomStringFormat<"hostname">; + | $ZodCustomStringFormat<"hostname"> + | $ZodScript; diff --git a/packages/zod/src/v4/mini/schemas.ts b/packages/zod/src/v4/mini/schemas.ts index 75095cf615..312bd491ac 100644 --- a/packages/zod/src/v4/mini/schemas.ts +++ b/packages/zod/src/v4/mini/schemas.ts @@ -227,6 +227,23 @@ export function emoji(params?: string | core.$ZodEmojiParams): ZodMiniEmoji { return core._emoji(ZodMiniEmoji, params); } +// ZodMiniScript +export interface ZodMiniScript extends _ZodMiniString { + // _zod: core.$ZodScriptInternals; +} +export const ZodMiniScript: core.$constructor = /*@__PURE__*/ core.$constructor( + "ZodMiniScript", + (inst, def) => { + core.$ZodScript.init(inst, def); + ZodMiniStringFormat.init(inst, def); + } +); + +// @__NO_SIDE_EFFECTS__ +export function script(name: string, params?: string | core.$ZodScriptParams): ZodMiniScript { + return core._script(ZodMiniScript, name, params); +} + // ZodMiniNanoID export interface ZodMiniNanoID extends _ZodMiniString { // _zod: core.$ZodNanoIDInternals;