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
-
+
|
@@ -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,