diff --git a/README.md b/README.md
index a5a7c54c2..f7331d97d 100644
--- a/README.md
+++ b/README.md
@@ -735,6 +735,7 @@ z.string().cuid2();
z.string().ulid();
z.string().regex(regex);
z.string().includes(string);
+z.string().hostname();
z.string().startsWith(string);
z.string().endsWith(string);
z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed
@@ -774,6 +775,7 @@ z.string().url({ message: "Invalid url" });
z.string().emoji({ message: "Contains non-emoji characters" });
z.string().uuid({ message: "Invalid UUID" });
z.string().includes("tuna", { message: "Must include tuna" });
+z.string().hostname({ message: "Must provide a valid hostname" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });
z.string().datetime({ message: "Invalid datetime string! Must be UTC." });
diff --git a/README_ZH.md b/README_ZH.md
index 0c9ab46a0..97dae8447 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -483,6 +483,7 @@ z.string().ulid();
z.string().duration();
z.string().regex(regex);
z.string().includes(string);
+z.string().hostname();
z.string().startsWith(string);
z.string().endsWith(string);
z.string().datetime(); // ISO 8601;默认值为无 UTC 偏移,选项见下文
@@ -516,6 +517,7 @@ z.string().url({ message: "Invalid url" });
z.string().emoji({ message: "Contains non-emoji characters" });
z.string().uuid({ message: "Invalid UUID" });
z.string().includes("tuna", { message: "Must include tuna" });
+z.string().hostname({ message: "Must provide a valid hostname" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });
z.string().datetime({ message: "Invalid datetime string! Must be UTC." });
diff --git a/deno/lib/README.md b/deno/lib/README.md
index 84ca28ad4..f7331d97d 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
-
+
@@ -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,6 +735,7 @@ z.string().cuid2();
z.string().ulid();
z.string().regex(regex);
z.string().includes(string);
+z.string().hostname();
z.string().startsWith(string);
z.string().endsWith(string);
z.string().datetime(); // ISO 8601; by default only `Z` timezone allowed
@@ -773,6 +775,7 @@ z.string().url({ message: "Invalid url" });
z.string().emoji({ message: "Contains non-emoji characters" });
z.string().uuid({ message: "Invalid UUID" });
z.string().includes("tuna", { message: "Must include tuna" });
+z.string().hostname({ message: "Must provide a valid hostname" });
z.string().startsWith("https://", { message: "Must provide secure URL" });
z.string().endsWith(".com", { message: "Only .com domains allowed" });
z.string().datetime({ message: "Invalid datetime string! Must be UTC." });
diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts
index e757cd8ba..4fddd2785 100644
--- a/deno/lib/ZodError.ts
+++ b/deno/lib/ZodError.ts
@@ -106,6 +106,7 @@ export type StringValidation =
| "base64"
| { includes: string; position?: number }
| { startsWith: string }
+ | { hostname: string }
| { endsWith: string };
export interface ZodInvalidStringIssue extends ZodIssueBase {
diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts
index b70152750..0dcba7d50 100644
--- a/deno/lib/__tests__/string.test.ts
+++ b/deno/lib/__tests__/string.test.ts
@@ -12,6 +12,7 @@ const includes = z.string().includes("includes");
const includesFromIndex2 = z.string().includes("includes", { position: 2 });
const startsWith = z.string().startsWith("startsWith");
const endsWith = z.string().endsWith("endsWith");
+const hostname = z.string().hostname();
test("passing validations", () => {
minFive.parse("12345");
@@ -24,6 +25,13 @@ test("passing validations", () => {
includesFromIndex2.parse("XXXincludesXX");
startsWith.parse("startsWithX");
endsWith.parse("XendsWith");
+ hostname.parse("developer.mozilla.org");
+ hostname.parse("hello.world.example.com");
+ hostname.parse("www.google.com");
+ hostname.parse("[2001:db8::ff00:42:8329]");
+ hostname.parse("192.168.1.1");
+ hostname.parse("xn--d1acj3b.com");
+ hostname.parse("xn--d1acj3b.org");
});
test("failing validations", () => {
@@ -36,6 +44,14 @@ test("failing validations", () => {
expect(() => includesFromIndex2.parse("XincludesXX")).toThrow();
expect(() => startsWith.parse("x")).toThrow();
expect(() => endsWith.parse("x")).toThrow();
+ expect(() => hostname.parse("ht!tp://invalid.com")).toThrow();
+ expect(() => hostname.parse("xn--d1acj3b")).toThrow();
+ expect(() => hostname.parse("xn--d1acj3b..com")).toThrow();
+ expect(() => hostname.parse("ex@mple.com")).toThrow();
+ expect(() => hostname.parse("[2001:db8::zzzz]")).toThrow();
+ expect(() => hostname.parse("exa mple.com")).toThrow();
+ expect(() => hostname.parse("-example.com")).toThrow();
+ expect(() => hostname.parse("example..com")).toThrow();
});
test("email validations", () => {
diff --git a/deno/lib/locales/en.ts b/deno/lib/locales/en.ts
index 0665af275..25fa97a1f 100644
--- a/deno/lib/locales/en.ts
+++ b/deno/lib/locales/en.ts
@@ -55,6 +55,8 @@ const errorMap: ZodErrorMap = (issue, _ctx) => {
}
} else if ("startsWith" in issue.validation) {
message = `Invalid input: must start with "${issue.validation.startsWith}"`;
+ } else if ("hostname" in issue.validation) {
+ message = `Invalid input: must be a valid hostname`;
} else if ("endsWith" in issue.validation) {
message = `Invalid input: must end with "${issue.validation.endsWith}"`;
} else {
diff --git a/deno/lib/types.ts b/deno/lib/types.ts
index d634e524f..187aa5ba2 100644
--- a/deno/lib/types.ts
+++ b/deno/lib/types.ts
@@ -540,6 +540,7 @@ export type ZodStringCheck =
| { kind: "includes"; value: string; position?: number; message?: string }
| { kind: "cuid2"; message?: string }
| { kind: "ulid"; message?: string }
+ | { kind: "hostname"; message?: string }
| { kind: "startsWith"; value: string; message?: string }
| { kind: "endsWith"; value: string; message?: string }
| { kind: "regex"; regex: RegExp; message?: string }
@@ -855,6 +856,29 @@ export class ZodString extends ZodType {
input.data = input.data.toLowerCase();
} else if (check.kind === "toUpperCase") {
input.data = input.data.toUpperCase();
+ } else if (check.kind === "hostname") {
+ const domainNameRegex =
+ /^(?!-)(?!.*--)(?!.*\.\.)(?!.*\.$)[a-zA-Z0-9-]{1,63}(? {
});
}
+ hostname(options?: { message?: string }) {
+ return this._addCheck({
+ kind: "hostname",
+ ...errorUtil.errToObj(options?.message),
+ });
+ }
+
startsWith(value: string, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "startsWith",
diff --git a/src/ZodError.ts b/src/ZodError.ts
index c1f7aa3ee..2803d4fc5 100644
--- a/src/ZodError.ts
+++ b/src/ZodError.ts
@@ -106,6 +106,7 @@ export type StringValidation =
| "base64"
| { includes: string; position?: number }
| { startsWith: string }
+ | { hostname: string }
| { endsWith: string };
export interface ZodInvalidStringIssue extends ZodIssueBase {
diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts
index 24f10ee45..76ba84965 100644
--- a/src/__tests__/string.test.ts
+++ b/src/__tests__/string.test.ts
@@ -11,6 +11,7 @@ const includes = z.string().includes("includes");
const includesFromIndex2 = z.string().includes("includes", { position: 2 });
const startsWith = z.string().startsWith("startsWith");
const endsWith = z.string().endsWith("endsWith");
+const hostname = z.string().hostname();
test("passing validations", () => {
minFive.parse("12345");
@@ -23,6 +24,13 @@ test("passing validations", () => {
includesFromIndex2.parse("XXXincludesXX");
startsWith.parse("startsWithX");
endsWith.parse("XendsWith");
+ hostname.parse("developer.mozilla.org");
+ hostname.parse("hello.world.example.com");
+ hostname.parse("www.google.com");
+ hostname.parse("[2001:db8::ff00:42:8329]");
+ hostname.parse("192.168.1.1");
+ hostname.parse("xn--d1acj3b.com");
+ hostname.parse("xn--d1acj3b.org");
});
test("failing validations", () => {
@@ -35,6 +43,14 @@ test("failing validations", () => {
expect(() => includesFromIndex2.parse("XincludesXX")).toThrow();
expect(() => startsWith.parse("x")).toThrow();
expect(() => endsWith.parse("x")).toThrow();
+ expect(() => hostname.parse("ht!tp://invalid.com")).toThrow();
+ expect(() => hostname.parse("xn--d1acj3b")).toThrow();
+ expect(() => hostname.parse("xn--d1acj3b..com")).toThrow();
+ expect(() => hostname.parse("ex@mple.com")).toThrow();
+ expect(() => hostname.parse("[2001:db8::zzzz]")).toThrow();
+ expect(() => hostname.parse("exa mple.com")).toThrow();
+ expect(() => hostname.parse("-example.com")).toThrow();
+ expect(() => hostname.parse("example..com")).toThrow();
});
test("email validations", () => {
diff --git a/src/locales/en.ts b/src/locales/en.ts
index 11325a95b..b48ef979d 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -55,6 +55,8 @@ const errorMap: ZodErrorMap = (issue, _ctx) => {
}
} else if ("startsWith" in issue.validation) {
message = `Invalid input: must start with "${issue.validation.startsWith}"`;
+ } else if ("hostname" in issue.validation) {
+ message = `Invalid input: must be a valid hostname`;
} else if ("endsWith" in issue.validation) {
message = `Invalid input: must end with "${issue.validation.endsWith}"`;
} else {
diff --git a/src/types.ts b/src/types.ts
index 0767073c5..a5204b8fa 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -540,6 +540,7 @@ export type ZodStringCheck =
| { kind: "includes"; value: string; position?: number; message?: string }
| { kind: "cuid2"; message?: string }
| { kind: "ulid"; message?: string }
+ | { kind: "hostname"; message?: string }
| { kind: "startsWith"; value: string; message?: string }
| { kind: "endsWith"; value: string; message?: string }
| { kind: "regex"; regex: RegExp; message?: string }
@@ -855,6 +856,29 @@ export class ZodString extends ZodType {
input.data = input.data.toLowerCase();
} else if (check.kind === "toUpperCase") {
input.data = input.data.toUpperCase();
+ } else if (check.kind === "hostname") {
+ const domainNameRegex =
+ /^(?!-)(?!.*--)(?!.*\.\.)(?!.*\.$)[a-zA-Z0-9-]{1,63}(? {
});
}
+ hostname(options?: { message?: string }) {
+ return this._addCheck({
+ kind: "hostname",
+ ...errorUtil.errToObj(options?.message),
+ });
+ }
+
startsWith(value: string, message?: errorUtil.ErrMessage) {
return this._addCheck({
kind: "startsWith",