Repository: colinhacks/zod
Pull request #4655 (2025-06-09 15:07:44)

Add Zod Sockets to Ecosystem
Starting v4.0.0 it supports Zod 4.


Patch:
diff --git a/packages/docs/components/ecosystem.tsx b/packages/docs/components/ecosystem.tsx
index 649edc10..25dd6048 100644
--- a/packages/docs/components/ecosystem.tsx
+++ b/packages/docs/components/ecosystem.tsx
@@ -37,6 +37,13 @@ const apiLibraries: ZodResource[] = [
     slug: "RobinTail/express-zod-api",
     v4: true,
   },
+  {
+    name: "Zod Sockets",
+    url: "https://github.com/RobinTail/zod-sockets",
+    description: "Socket.IO solution with I/O validation, an AsyncAPI generator, and a type-safe events map.",
+    slug: "RobinTail/zod-sockets",
+    v4: true,
+  },
 
   // https://github.com/honojs/middleware/tree/main/packages/zod-validator
   // {
diff --git a/packages/docs/content/api.mdx b/packages/docs/content/api.mdx
index 81657ce8..a3bb3138 100644
--- a/packages/docs/content/api.mdx
+++ b/packages/docs/content/api.mdx
@@ -2358,7 +2358,7 @@ type Cat = { name: string };
 type Dog = { name: string };
 
 const pluto: Dog = { name: "pluto" };
-const simba: Cat = fido; // works fine
+const simba: Cat = pluto; // works fine
 ```
 
 In some cases, it can be desirable to simulate [nominal typing](https://en.wikipedia.org/wiki/Nominal_type_system) inside TypeScript. This can be achieved with *branded types* (also known as "opaque types").
diff --git a/packages/zod/package.json b/packages/zod/package.json
index e198e2bd..09344fc2 100644
--- a/packages/zod/package.json
+++ b/packages/zod/package.json
@@ -1,6 +1,6 @@
 {
   "name": "zod",
-  "version": "3.25.56",
+  "version": "3.25.57",
   "type": "module",
   "author": "Colin McDonnell <zod@colinhacks.com>",
   "description": "TypeScript-first schema declaration and validation library with static type inference",
diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts
index 50cb0547..b1cb77f6 100644
--- a/packages/zod/src/v4/classic/schemas.ts
+++ b/packages/zod/src/v4/classic/schemas.ts
@@ -1984,14 +1984,15 @@ function _instanceof<T extends typeof util.Class>(
 export { _instanceof as instanceof };
 
 // stringbool
-export const stringbool: (_params?: string | core.$ZodStringBoolParams) => ZodPipe<ZodUnknown, ZodBoolean> = (
-  ...args
-) =>
+export const stringbool: (
+  _params?: string | core.$ZodStringBoolParams
+) => ZodPipe<ZodPipe<ZodString, ZodTransform<boolean, string>>, ZodBoolean> = (...args) =>
   core._stringbool(
     {
       Pipe: ZodPipe,
       Boolean: ZodBoolean,
-      Unknown: ZodUnknown,
+      String: ZodString,
+      Transform: ZodTransform,
     },
     ...args
   ) as any;
diff --git a/packages/zod/src/v4/core/api.ts b/packages/zod/src/v4/core/api.ts
index 2e9da61b..ab6efb75 100644
--- a/packages/zod/src/v4/core/api.ts
+++ b/packages/zod/src/v4/core/api.ts
@@ -1478,9 +1478,9 @@ export interface $ZodStringBoolParams extends TypeParams {
   truthy?: string[];
   falsy?: string[];
   /**
-   * Options `"sensitive"`, `"insensitive"`
+   * Options: `"sensitive"`, `"insensitive"`
    *
-   * Defaults to `"insensitive"`
+   * @default `"insensitive"`
    */
   case?: "sensitive" | "insensitive" | undefined;
 }
@@ -1489,66 +1489,69 @@ export function _stringbool(
   Classes: {
     Pipe?: typeof schemas.$ZodPipe;
     Boolean?: typeof schemas.$ZodBoolean;
-    Unknown?: typeof schemas.$ZodUnknown;
+    Transform?: typeof schemas.$ZodTransform;
+    String?: typeof schemas.$ZodString;
   },
   _params?: string | $ZodStringBoolParams
-): schemas.$ZodPipe<schemas.$ZodUnknown, schemas.$ZodBoolean<boolean>> {
+): schemas.$ZodPipe<
+  schemas.$ZodPipe<schemas.$ZodString, schemas.$ZodTransform<boolean, string>>,
+  schemas.$ZodBoolean<boolean>
+> {
   const { case: _case, error, truthy, falsy } = util.normalizeParams(_params);
 
-  const trueValues = new Set(truthy ?? ["true", "1", "yes", "on", "y", "enabled"]);
-  const falseValues = new Set(falsy ?? ["false", "0", "no", "off", "n", "disabled"]);
+  let truthyArray = truthy ?? ["true", "1", "yes", "on", "y", "enabled"];
+  let falsyArray = falsy ?? ["false", "0", "no", "off", "n", "disabled"];
+  if (_case !== "sensitive") {
+    truthyArray = truthyArray.map((v) => (typeof v === "string" ? v.toLowerCase() : v));
+    falsyArray = falsyArray.map((v) => (typeof v === "string" ? v.toLowerCase() : v));
+  }
+
+  const truthySet = new Set(truthyArray);
+  const falsySet = new Set(falsyArray);
 
   const _Pipe = Classes.Pipe ?? schemas.$ZodPipe;
   const _Boolean = Classes.Boolean ?? schemas.$ZodBoolean;
-  const _Unknown = Classes.Unknown ?? schemas.$ZodUnknown;
+  const _String = Classes.String ?? schemas.$ZodString;
+  const _Transform = Classes.Transform ?? schemas.$ZodTransform;
 
-  const inst = new _Unknown({
-    type: "unknown",
-    checks: [
-      {
-        _zod: {
-          check: (ctx: any) => {
-            if (typeof ctx.value === "string") {
-              let data: string = ctx.value;
-              if (_case !== "sensitive") data = data.toLowerCase();
-              if (trueValues.has(data)) {
-                ctx.value = true;
-              } else if (falseValues.has(data)) {
-                ctx.value = false;
-              } else {
-                ctx.issues.push({
-                  code: "invalid_value",
-                  expected: "stringbool",
-                  values: [...trueValues, ...falseValues],
-                  input: ctx.value,
-                  inst,
-                });
-              }
-            } else {
-              ctx.issues.push({
-                code: "invalid_type",
-                expected: "string",
-                input: ctx.value,
-              });
-            }
-          },
-          def: {
-            check: "custom",
-          },
-          onattach: [],
-        },
-      },
-    ],
+  const tx = new _Transform({
+    type: "transform",
+    transform: (input, payload: schemas.ParsePayload<unknown>) => {
+      let data: string = input as string;
+      if (_case !== "sensitive") data = data.toLowerCase();
+      if (truthySet.has(data)) {
+        return true;
+      } else if (falsySet.has(data)) {
+        return false;
+      } else {
+        payload.issues.push({
+          code: "invalid_value",
+          expected: "stringbool",
+          values: [...truthySet, ...falsySet],
+          input: payload.value,
+          inst: tx,
+        });
+        return {} as never;
+      }
+    },
+    error,
+  });
+
+  const innerPipe = new _Pipe({
+    type: "pipe",
+    in: new _String({ type: "string", error }),
+    out: tx,
     error,
   });
 
-  return new _Pipe({
+  const outerPipe = new _Pipe({
     type: "pipe",
-    in: inst,
+    in: innerPipe,
     out: new _Boolean({
       type: "boolean",
       error,
     }),
     error,
-  }) as any;
+  });
+  return outerPipe as any;
 }
diff --git a/packages/zod/src/v4/mini/schemas.ts b/packages/zod/src/v4/mini/schemas.ts
index af968fcb..dfef934e 100644
--- a/packages/zod/src/v4/mini/schemas.ts
+++ b/packages/zod/src/v4/mini/schemas.ts
@@ -1507,12 +1507,13 @@ export { _instanceof as instanceof };
 // stringbool
 export const stringbool: (
   _params?: string | core.$ZodStringBoolParams
-) => ZodMiniPipe<ZodMiniUnknown, ZodMiniBoolean<boolean>> = (...args) =>
+) => ZodMiniPipe<ZodMiniPipe<ZodMiniString, ZodMiniTransform<boolean, string>>, ZodMiniBoolean> = (...args) =>
   core._stringbool(
     {
       Pipe: ZodMiniPipe,
       Boolean: ZodMiniBoolean,
-      Unknown: ZodMiniUnknown,
+      String: ZodMiniString,
+      Transform: ZodMiniTransform,
     },
     ...args
   ) as any;
diff --git a/play.ts b/play.ts
index 1a6bbe68..41eb006b 100644
--- a/play.ts
+++ b/play.ts
@@ -1,8 +1,3 @@
 import { z } from "zod/v4";
 
-export const SegmentFilter = z.object({
-  type: z.literal("segment"),
-  id: z.string(),
-});
-
-export type SegmentFilter = z.infer<typeof SegmentFilter>;
+z;


Test patch:
diff --git a/packages/zod/src/v4/classic/tests/stringbool.test.ts b/packages/zod/src/v4/classic/tests/stringbool.test.ts
index f0cd31ea..2fe6c913 100644
--- a/packages/zod/src/v4/classic/tests/stringbool.test.ts
+++ b/packages/zod/src/v4/classic/tests/stringbool.test.ts
@@ -5,6 +5,8 @@ test("z.stringbool", () => {
   const a = z.stringbool();
   type a = z.infer<typeof a>;
   expectTypeOf<a>().toEqualTypeOf<boolean>();
+  type a_in = z.input<typeof a>;
+  expectTypeOf<a_in>().toEqualTypeOf<string>();
 
   expect(z.parse(a, "true")).toEqual(true);
   expect(z.parse(a, "yes")).toEqual(true);
@@ -28,20 +30,31 @@ test("z.stringbool", () => {
   expect(z.safeParse(a, {})).toMatchObject({ success: false });
   expect(z.safeParse(a, true)).toMatchObject({ success: false });
   expect(z.safeParse(a, false)).toMatchObject({ success: false });
+});
 
+test("custom values", () => {
   const b = z.stringbool({
     truthy: ["y"],
-    falsy: ["n"],
+    falsy: ["N"],
   });
   expect(z.parse(b, "y")).toEqual(true);
+  expect(z.parse(b, "Y")).toEqual(true);
   expect(z.parse(b, "n")).toEqual(false);
+  expect(z.parse(b, "N")).toEqual(false);
   expect(z.safeParse(b, "true")).toMatchObject({ success: false });
   expect(z.safeParse(b, "false")).toMatchObject({ success: false });
+});
 
+test("custom values - case sensitive", () => {
   const c = z.stringbool({
+    truthy: ["y"],
+    falsy: ["N"],
     case: "sensitive",
   });
-  expect(z.parse(c, "true")).toEqual(true);
+  expect(z.parse(c, "y")).toEqual(true);
+  expect(z.safeParse(c, "Y")).toMatchObject({ success: false });
+  expect(z.parse(c, "N")).toEqual(false);
+  expect(z.safeParse(c, "n")).toMatchObject({ success: false });
   expect(z.safeParse(c, "TRUE")).toMatchObject({ success: false });
 });