From af61c41ab8b2686c010091b83d2ef481fcfd3dc3 Mon Sep 17 00:00:00 2001
From: Luca De Santis <lucaroma2705@gmail.com>
Date: Sun, 19 Feb 2023 19:16:08 +0100
Subject: [PATCH 1/5] Add base implementation of string ip validation

For now check only IPv4
---
 deno/lib/README.md                | 12 +++++------
 deno/lib/ZodError.ts              |  1 +
 deno/lib/__tests__/string.test.ts | 26 ++++++++++++++++++++++
 deno/lib/types.ts                 | 36 +++++++++++++++++++++++++------
 src/ZodError.ts                   |  1 +
 src/__tests__/string.test.ts      | 26 ++++++++++++++++++++++
 src/types.ts                      | 36 +++++++++++++++++++++++++------
 7 files changed, 118 insertions(+), 20 deletions(-)

diff --git a/deno/lib/README.md b/deno/lib/README.md
index 6c2711ec5..065fca695 100644
--- a/deno/lib/README.md
+++ b/deno/lib/README.md
@@ -2349,14 +2349,14 @@ makeSchemaOptional(z.number());
 Zod provides a subclass of Error called `ZodError`. ZodErrors contain an `issues` array containing detailed information about the validation problems.
 
 ```ts
-const data = z
+const result = z
   .object({
     name: z.string(),
   })
   .safeParse({ name: 12 });
 
-if (!data.success) {
-  data.error.issues;
+if (!result.success) {
+  result.error.issues;
   /* [
       {
         "code": "invalid_type",
@@ -2378,14 +2378,14 @@ Zod's error reporting emphasizes _completeness_ and _correctness_. If you are lo
 You can use the `.format()` method to convert this error into a nested object.
 
 ```ts
-const data = z
+const result = z
   .object({
     name: z.string(),
   })
   .safeParse({ name: 12 });
 
-if (!data.success) {
-  const formatted = data.error.format();
+if (!result.success) {
+  const formatted = result.error.format();
   /* {
     name: { _errors: [ 'Expected string, received number' ] }
   } */
diff --git a/deno/lib/ZodError.ts b/deno/lib/ZodError.ts
index 365cb3cae..dcf4cc18a 100644
--- a/deno/lib/ZodError.ts
+++ b/deno/lib/ZodError.ts
@@ -97,6 +97,7 @@ export type StringValidation =
   | "cuid"
   | "cuid2"
   | "datetime"
+  | "ip"
   | { startsWith: string }
   | { endsWith: string };
 
diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts
index 3df55f7b2..c85bd8422 100644
--- a/deno/lib/__tests__/string.test.ts
+++ b/deno/lib/__tests__/string.test.ts
@@ -230,30 +230,42 @@ test("checks getters", () => {
   expect(z.string().email().isCUID).toEqual(false);
   expect(z.string().email().isCUID2).toEqual(false);
   expect(z.string().email().isUUID).toEqual(false);
+  expect(z.string().email().isIP).toEqual(false);
 
   expect(z.string().url().isEmail).toEqual(false);
   expect(z.string().url().isURL).toEqual(true);
   expect(z.string().url().isCUID).toEqual(false);
   expect(z.string().url().isCUID2).toEqual(false);
   expect(z.string().url().isUUID).toEqual(false);
+  expect(z.string().url().isIP).toEqual(false);
 
   expect(z.string().cuid().isEmail).toEqual(false);
   expect(z.string().cuid().isURL).toEqual(false);
   expect(z.string().cuid().isCUID).toEqual(true);
   expect(z.string().cuid().isCUID2).toEqual(false);
   expect(z.string().cuid().isUUID).toEqual(false);
+  expect(z.string().cuid().isIP).toEqual(false);
 
   expect(z.string().cuid2().isEmail).toEqual(false);
   expect(z.string().cuid2().isURL).toEqual(false);
   expect(z.string().cuid2().isCUID).toEqual(false);
   expect(z.string().cuid2().isCUID2).toEqual(true);
   expect(z.string().cuid2().isUUID).toEqual(false);
+  expect(z.string().cuid2().isIP).toEqual(false);
 
   expect(z.string().uuid().isEmail).toEqual(false);
   expect(z.string().uuid().isURL).toEqual(false);
   expect(z.string().uuid().isCUID).toEqual(false);
   expect(z.string().uuid().isCUID2).toEqual(false);
   expect(z.string().uuid().isUUID).toEqual(true);
+  expect(z.string().uuid().isIP).toEqual(false);
+
+  expect(z.string().ip().isEmail).toEqual(false);
+  expect(z.string().ip().isURL).toEqual(false);
+  expect(z.string().ip().isCUID).toEqual(false);
+  expect(z.string().ip().isCUID2).toEqual(false);
+  expect(z.string().ip().isUUID).toEqual(false);
+  expect(z.string().ip().isIP).toEqual(true);
 });
 
 test("min max getters", () => {
@@ -359,3 +371,17 @@ test("datetime parsing", () => {
     datetimeOffset4Ms.parse("2020-10-14T17:42:29.124+00:00")
   ).toThrow();
 });
+
+test("IP validation", () => {
+  const ip = z.string().ip();
+
+  expect(ip.safeParse("192.168.1.1").success).toBe(true);
+  expect(ip.safeParse("255.255.255.255").success).toBe(true);
+  expect(ip.safeParse("0.0.0.0").success).toBe(true);
+
+  expect(ip.safeParse("256.0.1.1").success).toBe(false);
+  expect(ip.safeParse("-1.53.78.1").success).toBe(false);
+  expect(ip.safeParse("0.0.0").success).toBe(false);
+  expect(ip.safeParse("128.44.1.0.5").success).toBe(false);
+  expect(ip.safeParse("1.1..1").success).toBe(false);
+})
\ No newline at end of file
diff --git a/deno/lib/types.ts b/deno/lib/types.ts
index df6c1391d..8cc60c3ee 100644
--- a/deno/lib/types.ts
+++ b/deno/lib/types.ts
@@ -505,7 +505,8 @@ export type ZodStringCheck =
       offset: boolean;
       precision: number | null;
       message?: string;
-    };
+    }
+  | { kind: "ip"; message?: string };
 
 export interface ZodStringDef extends ZodTypeDef {
   checks: ZodStringCheck[];
@@ -532,6 +533,9 @@ const emailRegex =
 const emojiRegex =
   /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]|\uFE0E|\uFE0F)/;
 
+const ipv4Regex =
+  /^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/;
+
 // Adapted from https://stackoverflow.com/a/3143231
 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => {
   if (args.precision) {
@@ -750,6 +754,16 @@ export class ZodString extends ZodType<string, ZodStringDef> {
           });
           status.dirty();
         }
+      } else if (check.kind === "ip") {
+        if (!ipv4Regex.test(input.data)) {
+          ctx = this._getOrReturnCtx(input, ctx);
+          addIssueToContext(ctx, {
+            validation: "ip",
+            code: ZodIssueCode.invalid_string,
+            message: check.message,
+          });
+          status.dirty();
+        }
       } else {
         util.assertNever(check);
       }
@@ -794,6 +808,11 @@ export class ZodString extends ZodType<string, ZodStringDef> {
   cuid2(message?: errorUtil.ErrMessage) {
     return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) });
   }
+
+  ip(message?: errorUtil.ErrMessage) {
+    return this._addCheck({ kind: "ip", ...errorUtil.errToObj(message) });
+  }
+
   datetime(
     options?:
       | string
@@ -903,6 +922,9 @@ export class ZodString extends ZodType<string, ZodStringDef> {
   get isCUID2() {
     return !!this._def.checks.find((ch) => ch.kind === "cuid2");
   }
+  get isIP() {
+    return !!this._def.checks.find((ch) => ch.kind === "ip");
+  }
 
   get minLength() {
     let min: number | null = null;
@@ -3500,7 +3522,7 @@ export class ZodFunction<
     return this._def.returns;
   }
 
-  args<Items extends Parameters<typeof ZodTuple["create"]>[0]>(
+  args<Items extends Parameters<(typeof ZodTuple)["create"]>[0]>(
     ...items: Items
   ): ZodFunction<ZodTuple<Items, ZodUnknown>, Returns> {
     return new ZodFunction({
@@ -4618,18 +4640,18 @@ const oboolean = () => booleanType().optional();
 
 export const coerce = {
   string: ((arg) =>
-    ZodString.create({ ...arg, coerce: true })) as typeof ZodString["create"],
+    ZodString.create({ ...arg, coerce: true })) as (typeof ZodString)["create"],
   number: ((arg) =>
-    ZodNumber.create({ ...arg, coerce: true })) as typeof ZodNumber["create"],
+    ZodNumber.create({ ...arg, coerce: true })) as (typeof ZodNumber)["create"],
   boolean: ((arg) =>
     ZodBoolean.create({
       ...arg,
       coerce: true,
-    })) as typeof ZodBoolean["create"],
+    })) as (typeof ZodBoolean)["create"],
   bigint: ((arg) =>
-    ZodBigInt.create({ ...arg, coerce: true })) as typeof ZodBigInt["create"],
+    ZodBigInt.create({ ...arg, coerce: true })) as (typeof ZodBigInt)["create"],
   date: ((arg) =>
-    ZodDate.create({ ...arg, coerce: true })) as typeof ZodDate["create"],
+    ZodDate.create({ ...arg, coerce: true })) as (typeof ZodDate)["create"],
 };
 
 export {
diff --git a/src/ZodError.ts b/src/ZodError.ts
index 03a34432a..0833ae614 100644
--- a/src/ZodError.ts
+++ b/src/ZodError.ts
@@ -97,6 +97,7 @@ export type StringValidation =
   | "cuid"
   | "cuid2"
   | "datetime"
+  | "ip"
   | { startsWith: string }
   | { endsWith: string };
 
diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts
index 1f51004db..3f208d8e5 100644
--- a/src/__tests__/string.test.ts
+++ b/src/__tests__/string.test.ts
@@ -229,30 +229,42 @@ test("checks getters", () => {
   expect(z.string().email().isCUID).toEqual(false);
   expect(z.string().email().isCUID2).toEqual(false);
   expect(z.string().email().isUUID).toEqual(false);
+  expect(z.string().email().isIP).toEqual(false);
 
   expect(z.string().url().isEmail).toEqual(false);
   expect(z.string().url().isURL).toEqual(true);
   expect(z.string().url().isCUID).toEqual(false);
   expect(z.string().url().isCUID2).toEqual(false);
   expect(z.string().url().isUUID).toEqual(false);
+  expect(z.string().url().isIP).toEqual(false);
 
   expect(z.string().cuid().isEmail).toEqual(false);
   expect(z.string().cuid().isURL).toEqual(false);
   expect(z.string().cuid().isCUID).toEqual(true);
   expect(z.string().cuid().isCUID2).toEqual(false);
   expect(z.string().cuid().isUUID).toEqual(false);
+  expect(z.string().cuid().isIP).toEqual(false);
 
   expect(z.string().cuid2().isEmail).toEqual(false);
   expect(z.string().cuid2().isURL).toEqual(false);
   expect(z.string().cuid2().isCUID).toEqual(false);
   expect(z.string().cuid2().isCUID2).toEqual(true);
   expect(z.string().cuid2().isUUID).toEqual(false);
+  expect(z.string().cuid2().isIP).toEqual(false);
 
   expect(z.string().uuid().isEmail).toEqual(false);
   expect(z.string().uuid().isURL).toEqual(false);
   expect(z.string().uuid().isCUID).toEqual(false);
   expect(z.string().uuid().isCUID2).toEqual(false);
   expect(z.string().uuid().isUUID).toEqual(true);
+  expect(z.string().uuid().isIP).toEqual(false);
+
+  expect(z.string().ip().isEmail).toEqual(false);
+  expect(z.string().ip().isURL).toEqual(false);
+  expect(z.string().ip().isCUID).toEqual(false);
+  expect(z.string().ip().isCUID2).toEqual(false);
+  expect(z.string().ip().isUUID).toEqual(false);
+  expect(z.string().ip().isIP).toEqual(true);
 });
 
 test("min max getters", () => {
@@ -358,3 +370,17 @@ test("datetime parsing", () => {
     datetimeOffset4Ms.parse("2020-10-14T17:42:29.124+00:00")
   ).toThrow();
 });
+
+test("IP validation", () => {
+  const ip = z.string().ip();
+
+  expect(ip.safeParse("192.168.1.1").success).toBe(true);
+  expect(ip.safeParse("255.255.255.255").success).toBe(true);
+  expect(ip.safeParse("0.0.0.0").success).toBe(true);
+
+  expect(ip.safeParse("256.0.1.1").success).toBe(false);
+  expect(ip.safeParse("-1.53.78.1").success).toBe(false);
+  expect(ip.safeParse("0.0.0").success).toBe(false);
+  expect(ip.safeParse("128.44.1.0.5").success).toBe(false);
+  expect(ip.safeParse("1.1..1").success).toBe(false);
+})
\ No newline at end of file
diff --git a/src/types.ts b/src/types.ts
index 2af24c0a9..4a4f46665 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -505,7 +505,8 @@ export type ZodStringCheck =
       offset: boolean;
       precision: number | null;
       message?: string;
-    };
+    }
+  | { kind: "ip"; message?: string };
 
 export interface ZodStringDef extends ZodTypeDef {
   checks: ZodStringCheck[];
@@ -532,6 +533,9 @@ const emailRegex =
 const emojiRegex =
   /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]|\uFE0E|\uFE0F)/;
 
+const ipv4Regex =
+  /^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/;
+
 // Adapted from https://stackoverflow.com/a/3143231
 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => {
   if (args.precision) {
@@ -750,6 +754,16 @@ export class ZodString extends ZodType<string, ZodStringDef> {
           });
           status.dirty();
         }
+      } else if (check.kind === "ip") {
+        if (!ipv4Regex.test(input.data)) {
+          ctx = this._getOrReturnCtx(input, ctx);
+          addIssueToContext(ctx, {
+            validation: "ip",
+            code: ZodIssueCode.invalid_string,
+            message: check.message,
+          });
+          status.dirty();
+        }
       } else {
         util.assertNever(check);
       }
@@ -794,6 +808,11 @@ export class ZodString extends ZodType<string, ZodStringDef> {
   cuid2(message?: errorUtil.ErrMessage) {
     return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) });
   }
+
+  ip(message?: errorUtil.ErrMessage) {
+    return this._addCheck({ kind: "ip", ...errorUtil.errToObj(message) });
+  }
+
   datetime(
     options?:
       | string
@@ -903,6 +922,9 @@ export class ZodString extends ZodType<string, ZodStringDef> {
   get isCUID2() {
     return !!this._def.checks.find((ch) => ch.kind === "cuid2");
   }
+  get isIP() {
+    return !!this._def.checks.find((ch) => ch.kind === "ip");
+  }
 
   get minLength() {
     let min: number | null = null;
@@ -3500,7 +3522,7 @@ export class ZodFunction<
     return this._def.returns;
   }
 
-  args<Items extends Parameters<typeof ZodTuple["create"]>[0]>(
+  args<Items extends Parameters<(typeof ZodTuple)["create"]>[0]>(
     ...items: Items
   ): ZodFunction<ZodTuple<Items, ZodUnknown>, Returns> {
     return new ZodFunction({
@@ -4618,18 +4640,18 @@ const oboolean = () => booleanType().optional();
 
 export const coerce = {
   string: ((arg) =>
-    ZodString.create({ ...arg, coerce: true })) as typeof ZodString["create"],
+    ZodString.create({ ...arg, coerce: true })) as (typeof ZodString)["create"],
   number: ((arg) =>
-    ZodNumber.create({ ...arg, coerce: true })) as typeof ZodNumber["create"],
+    ZodNumber.create({ ...arg, coerce: true })) as (typeof ZodNumber)["create"],
   boolean: ((arg) =>
     ZodBoolean.create({
       ...arg,
       coerce: true,
-    })) as typeof ZodBoolean["create"],
+    })) as (typeof ZodBoolean)["create"],
   bigint: ((arg) =>
-    ZodBigInt.create({ ...arg, coerce: true })) as typeof ZodBigInt["create"],
+    ZodBigInt.create({ ...arg, coerce: true })) as (typeof ZodBigInt)["create"],
   date: ((arg) =>
-    ZodDate.create({ ...arg, coerce: true })) as typeof ZodDate["create"],
+    ZodDate.create({ ...arg, coerce: true })) as (typeof ZodDate)["create"],
 };
 
 export {

From d0cb77efc97c3a9b466c057b3f1a8a284d2f3aa5 Mon Sep 17 00:00:00 2001
From: Luca De Santis <lucaroma2705@gmail.com>
Date: Sun, 19 Feb 2023 22:55:57 +0100
Subject: [PATCH 2/5] Add IP version

If the version is not defined, the check goes
for a valid IP whether it is version 4 or 6
---
 deno/lib/__tests__/string.test.ts | 47 ++++++++++++++++++++++++-------
 deno/lib/types.ts                 | 25 +++++++++++++---
 src/__tests__/string.test.ts      | 47 ++++++++++++++++++++++++-------
 src/types.ts                      | 25 +++++++++++++---
 4 files changed, 116 insertions(+), 28 deletions(-)

diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts
index c85bd8422..770bba9f5 100644
--- a/deno/lib/__tests__/string.test.ts
+++ b/deno/lib/__tests__/string.test.ts
@@ -374,14 +374,41 @@ test("datetime parsing", () => {
 
 test("IP validation", () => {
   const ip = z.string().ip();
+  expect(ip.safeParse("122.122.122.122").success).toBe(true);
+
+  const ipv4 = z.string().ip({ version: "v4" });
+  expect(() => ipv4.parse("6097:adfa:6f0b:220d:db08:5021:6191:7990")).toThrow();
+
+  const ipv6 = z.string().ip({ version: "v6" });
+  expect(() => ipv6.parse("254.164.77.1")).toThrow();
+
+  /* For when IPv6 is implemented
+  const validIPs = [
+    "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c",
+    "9d4:c956:420f:5788:4339:9b3b:2418:75c3",
+    "a6ea::2454:a5ce:94.105.123.75",
+    "474f:4c83::4e40:a47:ff95:0cda",
+    "d329:0:25b4:db47:a9d1:0:4926:0000",
+    "114.71.82.94",
+    "0.0.0.0",
+    "37.85.236.115",
+  ];
 
-  expect(ip.safeParse("192.168.1.1").success).toBe(true);
-  expect(ip.safeParse("255.255.255.255").success).toBe(true);
-  expect(ip.safeParse("0.0.0.0").success).toBe(true);
-
-  expect(ip.safeParse("256.0.1.1").success).toBe(false);
-  expect(ip.safeParse("-1.53.78.1").success).toBe(false);
-  expect(ip.safeParse("0.0.0").success).toBe(false);
-  expect(ip.safeParse("128.44.1.0.5").success).toBe(false);
-  expect(ip.safeParse("1.1..1").success).toBe(false);
-})
\ No newline at end of file
+  const invalidIPs = [
+    "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af",
+    "8f69::c757:395e:976e::3441",
+    "54cb::473f:d516:0.255.256.22",
+    "54cb::473f:d516:192.168.1",
+    "256.0.4.4",
+    "-1.0.555.4",
+    "0.0.0.0.0",
+    "1.1.1",
+  ];
+  // no parameters check IPv4 or IPv6
+  const ipSchema = z.string().ip();
+  expect(validIPs.every((ip) => ipSchema.safeParse(ip).success)).toBe(true);
+  expect(
+    invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false)
+  ).toBe(true);
+  */
+});
diff --git a/deno/lib/types.ts b/deno/lib/types.ts
index 8cc60c3ee..1f3863d3a 100644
--- a/deno/lib/types.ts
+++ b/deno/lib/types.ts
@@ -486,6 +486,7 @@ export abstract class ZodType<
 //////////                     //////////
 /////////////////////////////////////////
 /////////////////////////////////////////
+export type IpVersion = "v4" | "v6";
 export type ZodStringCheck =
   | { kind: "min"; value: number; message?: string }
   | { kind: "max"; value: number; message?: string }
@@ -506,7 +507,7 @@ export type ZodStringCheck =
       precision: number | null;
       message?: string;
     }
-  | { kind: "ip"; message?: string };
+  | { kind: "ip"; version?: IpVersion; message?: string };
 
 export interface ZodStringDef extends ZodTypeDef {
   checks: ZodStringCheck[];
@@ -536,6 +537,8 @@ const emojiRegex =
 const ipv4Regex =
   /^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/;
 
+const ipv6Regex = /not_implemented/; //! not implemented
+
 // Adapted from https://stackoverflow.com/a/3143231
 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => {
   if (args.precision) {
@@ -569,6 +572,17 @@ const datetimeRegex = (args: { precision: number | null; offset: boolean }) => {
   }
 };
 
+function isValidIP(ip: string, version?: IpVersion) {
+  if ((version === "v4" || !version) && ipv4Regex.test(ip)) {
+    return true;
+  }
+  if ((version === "v6" || !version) && ipv6Regex.test(ip)) {
+    return true;
+  }
+
+  return false;
+}
+
 export class ZodString extends ZodType<string, ZodStringDef> {
   _parse(input: ParseInput): ParseReturnType<string> {
     if (this._def.coerce) {
@@ -755,7 +769,7 @@ export class ZodString extends ZodType<string, ZodStringDef> {
           status.dirty();
         }
       } else if (check.kind === "ip") {
-        if (!ipv4Regex.test(input.data)) {
+        if (!isValidIP(input.data, check.version)) {
           ctx = this._getOrReturnCtx(input, ctx);
           addIssueToContext(ctx, {
             validation: "ip",
@@ -809,8 +823,11 @@ export class ZodString extends ZodType<string, ZodStringDef> {
     return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) });
   }
 
-  ip(message?: errorUtil.ErrMessage) {
-    return this._addCheck({ kind: "ip", ...errorUtil.errToObj(message) });
+  ip(options?: string | { version?: "v4" | "v6"; message?: string }) {
+    if (typeof options === "string") {
+      return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
+    }
+    return this._addCheck({ kind: "ip", ...options });
   }
 
   datetime(
diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts
index 3f208d8e5..d10f9e716 100644
--- a/src/__tests__/string.test.ts
+++ b/src/__tests__/string.test.ts
@@ -373,14 +373,41 @@ test("datetime parsing", () => {
 
 test("IP validation", () => {
   const ip = z.string().ip();
+  expect(ip.safeParse("122.122.122.122").success).toBe(true);
+
+  const ipv4 = z.string().ip({ version: "v4" });
+  expect(() => ipv4.parse("6097:adfa:6f0b:220d:db08:5021:6191:7990")).toThrow();
+
+  const ipv6 = z.string().ip({ version: "v6" });
+  expect(() => ipv6.parse("254.164.77.1")).toThrow();
+
+  /* For when IPv6 is implemented
+  const validIPs = [
+    "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c",
+    "9d4:c956:420f:5788:4339:9b3b:2418:75c3",
+    "a6ea::2454:a5ce:94.105.123.75",
+    "474f:4c83::4e40:a47:ff95:0cda",
+    "d329:0:25b4:db47:a9d1:0:4926:0000",
+    "114.71.82.94",
+    "0.0.0.0",
+    "37.85.236.115",
+  ];
 
-  expect(ip.safeParse("192.168.1.1").success).toBe(true);
-  expect(ip.safeParse("255.255.255.255").success).toBe(true);
-  expect(ip.safeParse("0.0.0.0").success).toBe(true);
-
-  expect(ip.safeParse("256.0.1.1").success).toBe(false);
-  expect(ip.safeParse("-1.53.78.1").success).toBe(false);
-  expect(ip.safeParse("0.0.0").success).toBe(false);
-  expect(ip.safeParse("128.44.1.0.5").success).toBe(false);
-  expect(ip.safeParse("1.1..1").success).toBe(false);
-})
\ No newline at end of file
+  const invalidIPs = [
+    "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af",
+    "8f69::c757:395e:976e::3441",
+    "54cb::473f:d516:0.255.256.22",
+    "54cb::473f:d516:192.168.1",
+    "256.0.4.4",
+    "-1.0.555.4",
+    "0.0.0.0.0",
+    "1.1.1",
+  ];
+  // no parameters check IPv4 or IPv6
+  const ipSchema = z.string().ip();
+  expect(validIPs.every((ip) => ipSchema.safeParse(ip).success)).toBe(true);
+  expect(
+    invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false)
+  ).toBe(true);
+  */
+});
diff --git a/src/types.ts b/src/types.ts
index 4a4f46665..6fd31d40b 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -486,6 +486,7 @@ export abstract class ZodType<
 //////////                     //////////
 /////////////////////////////////////////
 /////////////////////////////////////////
+export type IpVersion = "v4" | "v6";
 export type ZodStringCheck =
   | { kind: "min"; value: number; message?: string }
   | { kind: "max"; value: number; message?: string }
@@ -506,7 +507,7 @@ export type ZodStringCheck =
       precision: number | null;
       message?: string;
     }
-  | { kind: "ip"; message?: string };
+  | { kind: "ip"; version?: IpVersion; message?: string };
 
 export interface ZodStringDef extends ZodTypeDef {
   checks: ZodStringCheck[];
@@ -536,6 +537,8 @@ const emojiRegex =
 const ipv4Regex =
   /^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/;
 
+const ipv6Regex = /not_implemented/; //! not implemented
+
 // Adapted from https://stackoverflow.com/a/3143231
 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => {
   if (args.precision) {
@@ -569,6 +572,17 @@ const datetimeRegex = (args: { precision: number | null; offset: boolean }) => {
   }
 };
 
+function isValidIP(ip: string, version?: IpVersion) {
+  if ((version === "v4" || !version) && ipv4Regex.test(ip)) {
+    return true;
+  }
+  if ((version === "v6" || !version) && ipv6Regex.test(ip)) {
+    return true;
+  }
+
+  return false;
+}
+
 export class ZodString extends ZodType<string, ZodStringDef> {
   _parse(input: ParseInput): ParseReturnType<string> {
     if (this._def.coerce) {
@@ -755,7 +769,7 @@ export class ZodString extends ZodType<string, ZodStringDef> {
           status.dirty();
         }
       } else if (check.kind === "ip") {
-        if (!ipv4Regex.test(input.data)) {
+        if (!isValidIP(input.data, check.version)) {
           ctx = this._getOrReturnCtx(input, ctx);
           addIssueToContext(ctx, {
             validation: "ip",
@@ -809,8 +823,11 @@ export class ZodString extends ZodType<string, ZodStringDef> {
     return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) });
   }
 
-  ip(message?: errorUtil.ErrMessage) {
-    return this._addCheck({ kind: "ip", ...errorUtil.errToObj(message) });
+  ip(options?: string | { version?: "v4" | "v6"; message?: string }) {
+    if (typeof options === "string") {
+      return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
+    }
+    return this._addCheck({ kind: "ip", ...options });
   }
 
   datetime(

From 4349981fdc0fda95a0afecfe58053a3063a325dc Mon Sep 17 00:00:00 2001
From: Luca De Santis <lucaroma2705@gmail.com>
Date: Sun, 19 Feb 2023 23:42:50 +0100
Subject: [PATCH 3/5] Add IP in docs

---
 README.md          | 27 +++++++++++++++++++++++++++
 deno/lib/README.md | 27 +++++++++++++++++++++++++++
 2 files changed, 54 insertions(+)

diff --git a/README.md b/README.md
index 065fca695..330bffd4b 100644
--- a/README.md
+++ b/README.md
@@ -594,6 +594,7 @@ z.string().startsWith(string);
 z.string().endsWith(string);
 z.string().trim(); // trim whitespace
 z.string().datetime(); // defaults to UTC, see below for options
+z.string().ip(); // defaults to IPv4 and IPv6, see below for options
 ```
 
 > Check out [validator.js](https://github.com/validatorjs/validator.js) for a bunch of other useful string validation functions that can be used in conjunction with [Refinements](#refine).
@@ -619,6 +620,7 @@ z.string().uuid({ message: "Invalid UUID" });
 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." });
+z.string().ip({ message: "Invalid IP address" });
 ```
 
 ### Datetime validation
@@ -656,6 +658,31 @@ datetime.parse("2020-01-01T00:00:00Z"); // fail
 datetime.parse("2020-01-01T00:00:00.123456Z"); // fail
 ```
 
+### IP address validation
+
+The `z.string().ip()` method by default validate IPv4 and IPv6.
+
+```ts
+const ip = z.string().ip();
+
+ip.parse("192.168.1.1"); // pass
+ip.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // pass
+ip.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:192.168.1.1"); // pass
+
+ip.parse("256.1.1.1"); // fail
+ip.parse("84d5:51a0:9114:gggg:4cfa:f2d7:1f12:7003"); // fail
+```
+
+You can additionally set the IP `version`.
+
+```ts
+const ipv4 = z.string().ip({ version: "v4" });
+ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003") // fail
+
+const ipv6 = z.string().ip({ version: "v6" });
+ipv6.parse("192.168.1.1"); // fail
+```
+
 ## Numbers
 
 You can customize certain error messages when creating a number schema.
diff --git a/deno/lib/README.md b/deno/lib/README.md
index 065fca695..330bffd4b 100644
--- a/deno/lib/README.md
+++ b/deno/lib/README.md
@@ -594,6 +594,7 @@ z.string().startsWith(string);
 z.string().endsWith(string);
 z.string().trim(); // trim whitespace
 z.string().datetime(); // defaults to UTC, see below for options
+z.string().ip(); // defaults to IPv4 and IPv6, see below for options
 ```
 
 > Check out [validator.js](https://github.com/validatorjs/validator.js) for a bunch of other useful string validation functions that can be used in conjunction with [Refinements](#refine).
@@ -619,6 +620,7 @@ z.string().uuid({ message: "Invalid UUID" });
 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." });
+z.string().ip({ message: "Invalid IP address" });
 ```
 
 ### Datetime validation
@@ -656,6 +658,31 @@ datetime.parse("2020-01-01T00:00:00Z"); // fail
 datetime.parse("2020-01-01T00:00:00.123456Z"); // fail
 ```
 
+### IP address validation
+
+The `z.string().ip()` method by default validate IPv4 and IPv6.
+
+```ts
+const ip = z.string().ip();
+
+ip.parse("192.168.1.1"); // pass
+ip.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // pass
+ip.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:192.168.1.1"); // pass
+
+ip.parse("256.1.1.1"); // fail
+ip.parse("84d5:51a0:9114:gggg:4cfa:f2d7:1f12:7003"); // fail
+```
+
+You can additionally set the IP `version`.
+
+```ts
+const ipv4 = z.string().ip({ version: "v4" });
+ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003") // fail
+
+const ipv6 = z.string().ip({ version: "v6" });
+ipv6.parse("192.168.1.1"); // fail
+```
+
 ## Numbers
 
 You can customize certain error messages when creating a number schema.

From 78a2eee225f1194ce6c5ff92829cd44379a70f7a Mon Sep 17 00:00:00 2001
From: Luca De Santis <lucaroma2705@gmail.com>
Date: Tue, 21 Feb 2023 16:39:48 +0100
Subject: [PATCH 4/5] Implement IPv6 validation

---
 deno/lib/__tests__/string.test.ts | 4 ++--
 deno/lib/types.ts                 | 3 ++-
 src/__tests__/string.test.ts      | 4 ++--
 src/types.ts                      | 3 ++-
 4 files changed, 8 insertions(+), 6 deletions(-)

diff --git a/deno/lib/__tests__/string.test.ts b/deno/lib/__tests__/string.test.ts
index 770bba9f5..b2b6437af 100644
--- a/deno/lib/__tests__/string.test.ts
+++ b/deno/lib/__tests__/string.test.ts
@@ -382,13 +382,13 @@ test("IP validation", () => {
   const ipv6 = z.string().ip({ version: "v6" });
   expect(() => ipv6.parse("254.164.77.1")).toThrow();
 
-  /* For when IPv6 is implemented
   const validIPs = [
     "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c",
     "9d4:c956:420f:5788:4339:9b3b:2418:75c3",
     "a6ea::2454:a5ce:94.105.123.75",
     "474f:4c83::4e40:a47:ff95:0cda",
     "d329:0:25b4:db47:a9d1:0:4926:0000",
+    "e48:10fb:1499:3e28:e4b6:dea5:4692:912c",
     "114.71.82.94",
     "0.0.0.0",
     "37.85.236.115",
@@ -396,6 +396,7 @@ test("IP validation", () => {
 
   const invalidIPs = [
     "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af",
+    "d5e7:7214:2b78::3906:85e6:53cc:709:32ba",
     "8f69::c757:395e:976e::3441",
     "54cb::473f:d516:0.255.256.22",
     "54cb::473f:d516:192.168.1",
@@ -410,5 +411,4 @@ test("IP validation", () => {
   expect(
     invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false)
   ).toBe(true);
-  */
 });
diff --git a/deno/lib/types.ts b/deno/lib/types.ts
index 1f3863d3a..5e759477b 100644
--- a/deno/lib/types.ts
+++ b/deno/lib/types.ts
@@ -537,7 +537,8 @@ const emojiRegex =
 const ipv4Regex =
   /^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/;
 
-const ipv6Regex = /not_implemented/; //! not implemented
+const ipv6Regex =
+  /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/;
 
 // Adapted from https://stackoverflow.com/a/3143231
 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => {
diff --git a/src/__tests__/string.test.ts b/src/__tests__/string.test.ts
index d10f9e716..39a54da2b 100644
--- a/src/__tests__/string.test.ts
+++ b/src/__tests__/string.test.ts
@@ -381,13 +381,13 @@ test("IP validation", () => {
   const ipv6 = z.string().ip({ version: "v6" });
   expect(() => ipv6.parse("254.164.77.1")).toThrow();
 
-  /* For when IPv6 is implemented
   const validIPs = [
     "1e5e:e6c8:daac:514b:114b:e360:d8c0:682c",
     "9d4:c956:420f:5788:4339:9b3b:2418:75c3",
     "a6ea::2454:a5ce:94.105.123.75",
     "474f:4c83::4e40:a47:ff95:0cda",
     "d329:0:25b4:db47:a9d1:0:4926:0000",
+    "e48:10fb:1499:3e28:e4b6:dea5:4692:912c",
     "114.71.82.94",
     "0.0.0.0",
     "37.85.236.115",
@@ -395,6 +395,7 @@ test("IP validation", () => {
 
   const invalidIPs = [
     "d329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af",
+    "d5e7:7214:2b78::3906:85e6:53cc:709:32ba",
     "8f69::c757:395e:976e::3441",
     "54cb::473f:d516:0.255.256.22",
     "54cb::473f:d516:192.168.1",
@@ -409,5 +410,4 @@ test("IP validation", () => {
   expect(
     invalidIPs.every((ip) => ipSchema.safeParse(ip).success === false)
   ).toBe(true);
-  */
 });
diff --git a/src/types.ts b/src/types.ts
index 6fd31d40b..1f9248154 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -537,7 +537,8 @@ const emojiRegex =
 const ipv4Regex =
   /^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/;
 
-const ipv6Regex = /not_implemented/; //! not implemented
+const ipv6Regex =
+  /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/;
 
 // Adapted from https://stackoverflow.com/a/3143231
 const datetimeRegex = (args: { precision: number | null; offset: boolean }) => {

From 1808f47f3b7af5959339c808bf151b3bef05099a Mon Sep 17 00:00:00 2001
From: Colin McDonnell <colinmcd94@gmail.com>
Date: Sun, 26 Feb 2023 13:14:58 -0800
Subject: [PATCH 5/5] Use errToObj and update readme

---
 README.md          |  4 +++-
 deno/lib/README.md |  4 +++-
 deno/lib/types.ts  | 17 +++++++----------
 src/types.ts       | 17 +++++++----------
 4 files changed, 20 insertions(+), 22 deletions(-)

diff --git a/README.md b/README.md
index 330bffd4b..923ba6aca 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,8 @@
 - [Coercion for primitives](#coercion-for-primitives)
 - [Literals](#literals)
 - [Strings](#strings)
+  - [Datetime](#datetime-validation)
+  - [IP](#ip-address-validation)
 - [Numbers](#numbers)
 - [NaNs](#nans)
 - [Booleans](#booleans)
@@ -677,7 +679,7 @@ You can additionally set the IP `version`.
 
 ```ts
 const ipv4 = z.string().ip({ version: "v4" });
-ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003") // fail
+ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // fail
 
 const ipv6 = z.string().ip({ version: "v6" });
 ipv6.parse("192.168.1.1"); // fail
diff --git a/deno/lib/README.md b/deno/lib/README.md
index 330bffd4b..923ba6aca 100644
--- a/deno/lib/README.md
+++ b/deno/lib/README.md
@@ -55,6 +55,8 @@
 - [Coercion for primitives](#coercion-for-primitives)
 - [Literals](#literals)
 - [Strings](#strings)
+  - [Datetime](#datetime-validation)
+  - [IP](#ip-address-validation)
 - [Numbers](#numbers)
 - [NaNs](#nans)
 - [Booleans](#booleans)
@@ -677,7 +679,7 @@ You can additionally set the IP `version`.
 
 ```ts
 const ipv4 = z.string().ip({ version: "v4" });
-ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003") // fail
+ipv4.parse("84d5:51a0:9114:1855:4cfa:f2d7:1f12:7003"); // fail
 
 const ipv6 = z.string().ip({ version: "v6" });
 ipv6.parse("192.168.1.1"); // fail
diff --git a/deno/lib/types.ts b/deno/lib/types.ts
index 5e759477b..ac8523fb5 100644
--- a/deno/lib/types.ts
+++ b/deno/lib/types.ts
@@ -825,10 +825,7 @@ export class ZodString extends ZodType<string, ZodStringDef> {
   }
 
   ip(options?: string | { version?: "v4" | "v6"; message?: string }) {
-    if (typeof options === "string") {
-      return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
-    }
-    return this._addCheck({ kind: "ip", ...options });
+    return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
   }
 
   datetime(
@@ -3540,7 +3537,7 @@ export class ZodFunction<
     return this._def.returns;
   }
 
-  args<Items extends Parameters<(typeof ZodTuple)["create"]>[0]>(
+  args<Items extends Parameters<typeof ZodTuple["create"]>[0]>(
     ...items: Items
   ): ZodFunction<ZodTuple<Items, ZodUnknown>, Returns> {
     return new ZodFunction({
@@ -4658,18 +4655,18 @@ const oboolean = () => booleanType().optional();
 
 export const coerce = {
   string: ((arg) =>
-    ZodString.create({ ...arg, coerce: true })) as (typeof ZodString)["create"],
+    ZodString.create({ ...arg, coerce: true })) as typeof ZodString["create"],
   number: ((arg) =>
-    ZodNumber.create({ ...arg, coerce: true })) as (typeof ZodNumber)["create"],
+    ZodNumber.create({ ...arg, coerce: true })) as typeof ZodNumber["create"],
   boolean: ((arg) =>
     ZodBoolean.create({
       ...arg,
       coerce: true,
-    })) as (typeof ZodBoolean)["create"],
+    })) as typeof ZodBoolean["create"],
   bigint: ((arg) =>
-    ZodBigInt.create({ ...arg, coerce: true })) as (typeof ZodBigInt)["create"],
+    ZodBigInt.create({ ...arg, coerce: true })) as typeof ZodBigInt["create"],
   date: ((arg) =>
-    ZodDate.create({ ...arg, coerce: true })) as (typeof ZodDate)["create"],
+    ZodDate.create({ ...arg, coerce: true })) as typeof ZodDate["create"],
 };
 
 export {
diff --git a/src/types.ts b/src/types.ts
index 1f9248154..8a66c85a5 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -825,10 +825,7 @@ export class ZodString extends ZodType<string, ZodStringDef> {
   }
 
   ip(options?: string | { version?: "v4" | "v6"; message?: string }) {
-    if (typeof options === "string") {
-      return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
-    }
-    return this._addCheck({ kind: "ip", ...options });
+    return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) });
   }
 
   datetime(
@@ -3540,7 +3537,7 @@ export class ZodFunction<
     return this._def.returns;
   }
 
-  args<Items extends Parameters<(typeof ZodTuple)["create"]>[0]>(
+  args<Items extends Parameters<typeof ZodTuple["create"]>[0]>(
     ...items: Items
   ): ZodFunction<ZodTuple<Items, ZodUnknown>, Returns> {
     return new ZodFunction({
@@ -4658,18 +4655,18 @@ const oboolean = () => booleanType().optional();
 
 export const coerce = {
   string: ((arg) =>
-    ZodString.create({ ...arg, coerce: true })) as (typeof ZodString)["create"],
+    ZodString.create({ ...arg, coerce: true })) as typeof ZodString["create"],
   number: ((arg) =>
-    ZodNumber.create({ ...arg, coerce: true })) as (typeof ZodNumber)["create"],
+    ZodNumber.create({ ...arg, coerce: true })) as typeof ZodNumber["create"],
   boolean: ((arg) =>
     ZodBoolean.create({
       ...arg,
       coerce: true,
-    })) as (typeof ZodBoolean)["create"],
+    })) as typeof ZodBoolean["create"],
   bigint: ((arg) =>
-    ZodBigInt.create({ ...arg, coerce: true })) as (typeof ZodBigInt)["create"],
+    ZodBigInt.create({ ...arg, coerce: true })) as typeof ZodBigInt["create"],
   date: ((arg) =>
-    ZodDate.create({ ...arg, coerce: true })) as (typeof ZodDate)["create"],
+    ZodDate.create({ ...arg, coerce: true })) as typeof ZodDate["create"],
 };
 
 export {