diff --git a/README.md b/README.md index a5a7c54c2..92f3f4385 100644 --- a/README.md +++ b/README.md @@ -735,8 +735,8 @@ z.string().cuid2(); z.string().ulid(); z.string().regex(regex); z.string().includes(string); -z.string().startsWith(string); -z.string().endsWith(string); +z.string().startsWith(string | string[]); +z.string().endsWith(string | string[]); z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed z.string().ip(); // defaults to allow both IPv4 and IPv6 @@ -775,7 +775,13 @@ z.string().emoji({ message: "Contains non-emoji characters" }); z.string().uuid({ message: "Invalid UUID" }); z.string().includes("tuna", { message: "Must include tuna" }); z.string().startsWith("https://", { message: "Must provide secure URL" }); +z.string().startsWith(["https://", "http://"], { + message: "Must provide a valid URL starting with http:// or https://", +}); z.string().endsWith(".com", { message: "Only .com domains allowed" }); +z.string().endsWith([".com", ".org"], { + message: "Only .com or .org domains allowed", +}); z.string().datetime({ message: "Invalid datetime string! Must be UTC." }); z.string().date({ message: "Invalid date string!" }); z.string().time({ message: "Invalid time string!" }); diff --git a/README_ZH.md b/README_ZH.md index 0c9ab46a0..7245a4353 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -483,8 +483,8 @@ z.string().ulid(); z.string().duration(); z.string().regex(regex); z.string().includes(string); -z.string().startsWith(string); -z.string().endsWith(string); +z.string().startsWith(string | string[]); +z.string().endsWith(string | string[]); z.string().datetime(); // ISO 8601;默认值为无 UTC 偏移,选项见下文 z.string().ip(); // 默认为 IPv4 和 IPv6,选项见下文 @@ -517,7 +517,13 @@ z.string().emoji({ message: "Contains non-emoji characters" }); z.string().uuid({ message: "Invalid UUID" }); z.string().includes("tuna", { message: "Must include tuna" }); z.string().startsWith("https://", { message: "Must provide secure URL" }); +z.string().startsWith(["https://", "http://"], { + message: "Must provide a valid URL starting with http:// or https://", +}); z.string().endsWith(".com", { message: "Only .com domains allowed" }); +z.string().endsWith([".com", ".org"], { + message: "Only .com or .org domains allowed", +}); z.string().datetime({ message: "Invalid datetime string! Must be UTC." }); z.string().ip({ message: "Invalid IP address" }); ``` diff --git a/deno/lib/README.md b/deno/lib/README.md index 84ca28ad4..92f3f4385 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -252,7 +252,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod

- + speakeasy @@ -261,7 +261,7 @@ Sponsorship at any level is appreciated and encouraged. If you built a paid prod
SDKs & Terraform providers for your API
-
speakeasyapi.dev + speakeasy.com

@@ -490,6 +490,7 @@ There are a growing number of tools that are built atop or support Zod natively! - [`zod-prisma`](https://github.com/CarterGrimmeisen/zod-prisma): Generate Zod schemas from your Prisma schema. - [`Supervillain`](https://github.com/Southclaws/supervillain): Generate Zod schemas from your Go structs. - [`prisma-zod-generator`](https://github.com/omar-dulaimi/prisma-zod-generator): Emit Zod schemas from your Prisma schema. +- [`drizzle-zod`](https://orm.drizzle.team/docs/zod): Emit Zod schemas from your Drizzle schema. - [`prisma-trpc-generator`](https://github.com/omar-dulaimi/prisma-trpc-generator): Emit fully implemented tRPC routers and their validation schemas using Zod. - [`zod-prisma-types`](https://github.com/chrishoermann/zod-prisma-types) Create Zod types from your Prisma models. - [`quicktype`](https://app.quicktype.io/): Convert JSON objects and JSON schemas into Zod schemas. @@ -734,8 +735,8 @@ z.string().cuid2(); z.string().ulid(); z.string().regex(regex); z.string().includes(string); -z.string().startsWith(string); -z.string().endsWith(string); +z.string().startsWith(string | string[]); +z.string().endsWith(string | string[]); z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed z.string().ip(); // defaults to allow both IPv4 and IPv6 @@ -774,7 +775,13 @@ z.string().emoji({ message: "Contains non-emoji characters" }); z.string().uuid({ message: "Invalid UUID" }); z.string().includes("tuna", { message: "Must include tuna" }); z.string().startsWith("https://", { message: "Must provide secure URL" }); +z.string().startsWith(["https://", "http://"], { + message: "Must provide a valid URL starting with http:// or https://", +}); z.string().endsWith(".com", { message: "Only .com domains allowed" }); +z.string().endsWith([".com", ".org"], { + message: "Only .com or .org domains allowed", +}); z.string().datetime({ message: "Invalid datetime string! Must be UTC." }); z.string().date({ message: "Invalid date string!" }); z.string().time({ message: "Invalid time string!" }); diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts index b70152750..0ffd31d36 100644 --- a/deno/lib/__tests__/string.test.ts +++ b/deno/lib/__tests__/string.test.ts @@ -11,7 +11,9 @@ const nonempty = z.string().min(1, "nonempty"); const includes = z.string().includes("includes"); const includesFromIndex2 = z.string().includes("includes", { position: 2 }); const startsWith = z.string().startsWith("startsWith"); +const startsWithMultiple = z.string().startsWith(["foo_", "bar_", "baz_"]); const endsWith = z.string().endsWith("endsWith"); +const endsWithMultiple = z.string().endsWith(["foo_", "bar_", "baz_"]); test("passing validations", () => { minFive.parse("12345"); @@ -23,7 +25,9 @@ test("passing validations", () => { includes.parse("XincludesXX"); includesFromIndex2.parse("XXXincludesXX"); startsWith.parse("startsWithX"); + startsWithMultiple.parse("baz_12345"); endsWith.parse("XendsWith"); + endsWithMultiple.parse("12345baz_"); }); test("failing validations", () => { @@ -35,7 +39,9 @@ test("failing validations", () => { expect(() => includes.parse("XincludeXX")).toThrow(); expect(() => includesFromIndex2.parse("XincludesXX")).toThrow(); expect(() => startsWith.parse("x")).toThrow(); + expect(() => startsWithMultiple.parse("x")).toThrow(); expect(() => endsWith.parse("x")).toThrow(); + expect(() => endsWithMultiple.parse("x")).toThrow(); }); test("email validations", () => { diff --git a/deno/lib/types.ts b/deno/lib/types.ts index d634e524f..094ad3886 100644 --- a/deno/lib/types.ts +++ b/deno/lib/types.ts @@ -542,6 +542,8 @@ export type ZodStringCheck = | { kind: "ulid"; message?: string } | { kind: "startsWith"; value: string; message?: string } | { kind: "endsWith"; value: string; message?: string } + | { kind: "startsWith"; value: string | string[]; message?: string } + | { kind: "endsWith"; value: string | string[]; message?: string } | { kind: "regex"; regex: RegExp; message?: string } | { kind: "trim"; message?: string } | { kind: "toLowerCase"; message?: string } @@ -856,21 +858,35 @@ export class ZodString extends ZodType { } else if (check.kind === "toUpperCase") { input.data = input.data.toUpperCase(); } else if (check.kind === "startsWith") { - if (!(input.data as string).startsWith(check.value)) { + if ( + (typeof check.value === "string" && + !(input.data as string).startsWith(check.value)) || + (Array.isArray(check.value) && + !check.value.some((e: string) => + (input.data as string)?.startsWith(e) + )) + ) { ctx = this._getOrReturnCtx(input, ctx); addIssueToContext(ctx, { code: ZodIssueCode.invalid_string, - validation: { startsWith: check.value }, + validation: { startsWith: check.value.toString() }, message: check.message, }); status.dirty(); } } else if (check.kind === "endsWith") { - if (!(input.data as string).endsWith(check.value)) { + if ( + (typeof check.value === "string" && + !(input.data as string).endsWith(check.value)) || + (Array.isArray(check.value) && + !check.value.some((e: string) => + (input.data as string)?.endsWith(e) + )) + ) { ctx = this._getOrReturnCtx(input, ctx); addIssueToContext(ctx, { code: ZodIssueCode.invalid_string, - validation: { endsWith: check.value }, + validation: { endsWith: check.value.toString() }, message: check.message, }); status.dirty(); @@ -1082,7 +1098,7 @@ export class ZodString extends ZodType { }); } - startsWith(value: string, message?: errorUtil.ErrMessage) { + startsWith(value: string | string[], message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "startsWith", value: value, @@ -1090,7 +1106,7 @@ export class ZodString extends ZodType { }); } - endsWith(value: string, message?: errorUtil.ErrMessage) { + endsWith(value: string | string[], message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "endsWith", value: value, diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts index 24f10ee45..c4d156c35 100644 --- a/src/__tests__/string.test.ts +++ b/src/__tests__/string.test.ts @@ -10,7 +10,9 @@ const nonempty = z.string().min(1, "nonempty"); const includes = z.string().includes("includes"); const includesFromIndex2 = z.string().includes("includes", { position: 2 }); const startsWith = z.string().startsWith("startsWith"); +const startsWithMultiple = z.string().startsWith(["foo_", "bar_", "baz_"]); const endsWith = z.string().endsWith("endsWith"); +const endsWithMultiple = z.string().endsWith(["foo_", "bar_", "baz_"]); test("passing validations", () => { minFive.parse("12345"); @@ -22,7 +24,9 @@ test("passing validations", () => { includes.parse("XincludesXX"); includesFromIndex2.parse("XXXincludesXX"); startsWith.parse("startsWithX"); + startsWithMultiple.parse("baz_12345"); endsWith.parse("XendsWith"); + endsWithMultiple.parse("12345baz_"); }); test("failing validations", () => { @@ -34,7 +38,9 @@ test("failing validations", () => { expect(() => includes.parse("XincludeXX")).toThrow(); expect(() => includesFromIndex2.parse("XincludesXX")).toThrow(); expect(() => startsWith.parse("x")).toThrow(); + expect(() => startsWithMultiple.parse("x")).toThrow(); expect(() => endsWith.parse("x")).toThrow(); + expect(() => endsWithMultiple.parse("x")).toThrow(); }); test("email validations", () => { diff --git a/src/types.ts b/src/types.ts index 0767073c5..c420d8ea8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -542,6 +542,8 @@ export type ZodStringCheck = | { kind: "ulid"; message?: string } | { kind: "startsWith"; value: string; message?: string } | { kind: "endsWith"; value: string; message?: string } + | { kind: "startsWith"; value: string | string[]; message?: string } + | { kind: "endsWith"; value: string | string[]; message?: string } | { kind: "regex"; regex: RegExp; message?: string } | { kind: "trim"; message?: string } | { kind: "toLowerCase"; message?: string } @@ -856,21 +858,35 @@ export class ZodString extends ZodType { } else if (check.kind === "toUpperCase") { input.data = input.data.toUpperCase(); } else if (check.kind === "startsWith") { - if (!(input.data as string).startsWith(check.value)) { + if ( + (typeof check.value === "string" && + !(input.data as string).startsWith(check.value)) || + (Array.isArray(check.value) && + !check.value.some((e: string) => + (input.data as string)?.startsWith(e) + )) + ) { ctx = this._getOrReturnCtx(input, ctx); addIssueToContext(ctx, { code: ZodIssueCode.invalid_string, - validation: { startsWith: check.value }, + validation: { startsWith: check.value.toString() }, message: check.message, }); status.dirty(); } } else if (check.kind === "endsWith") { - if (!(input.data as string).endsWith(check.value)) { + if ( + (typeof check.value === "string" && + !(input.data as string).endsWith(check.value)) || + (Array.isArray(check.value) && + !check.value.some((e: string) => + (input.data as string)?.endsWith(e) + )) + ) { ctx = this._getOrReturnCtx(input, ctx); addIssueToContext(ctx, { code: ZodIssueCode.invalid_string, - validation: { endsWith: check.value }, + validation: { endsWith: check.value.toString() }, message: check.message, }); status.dirty(); @@ -1082,7 +1098,7 @@ export class ZodString extends ZodType { }); } - startsWith(value: string, message?: errorUtil.ErrMessage) { + startsWith(value: string | string[], message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "startsWith", value: value, @@ -1090,7 +1106,7 @@ export class ZodString extends ZodType { }); } - endsWith(value: string, message?: errorUtil.ErrMessage) { + endsWith(value: string | string[], message?: errorUtil.ErrMessage) { return this._addCheck({ kind: "endsWith", value: value,