Repository: colinhacks/zod
Pull request #4635 (2025-06-06 09:08:00)

Added Superforms for SvelteKit


Patch:
diff --git a/packages/docs/components/ecosystem.tsx b/packages/docs/components/ecosystem.tsx
index 649edc10..7a89faa2 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
   // {
@@ -61,6 +68,12 @@ const formIntegrations: ZodResource[] = [
       "A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js.",
     slug: "edmundhung/conform",
   },
+  {
+    name: "Superforms",
+    url: "https://superforms.rocks",
+    description: "Making SvelteKit forms a pleasure to use!",
+    slug: "ciscoheat/sveltekit-superforms",
+  },
 ];
 
 const zodToXConverters: ZodResource[] = [];
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/docs/content/packages/core.mdx b/packages/docs/content/packages/core.mdx
index 5ff29a8b..c7dd158e 100644
--- a/packages/docs/content/packages/core.mdx
+++ b/packages/docs/content/packages/core.mdx
@@ -418,7 +418,7 @@ export type $ZodIssue =
   | $ZodIssueCustom;
   ```
 
-For details on each type, refer to [the implementation](https://github.com/colinhacks/zod/blob/v4/packages/core/src/errors.ts).
+For details on each type, refer to [the implementation](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/errors.ts).
 
 
 {/* ## Best practices
diff --git a/packages/zod/README.md b/packages/zod/README.md
index 52ba194f..addbb833 100644
--- a/packages/zod/README.md
+++ b/packages/zod/README.md
@@ -138,7 +138,7 @@ When validation fails, the `.parse()` method will throw a `ZodError` instance wi
 try {
   Player.parse({ username: 42, xp: "100" });
 } catch (err) {
-  if (error instanceof z.ZodError) {
+  if (err instanceof z.ZodError) {
     err.issues;
     /* [
       {
diff --git a/packages/zod/package.json b/packages/zod/package.json
index 8041acf0..09344fc2 100644
--- a/packages/zod/package.json
+++ b/packages/zod/package.json
@@ -1,6 +1,6 @@
 {
   "name": "zod",
-  "version": "3.25.55",
+  "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 3ecd82e9..b1cb77f6 100644
--- a/packages/zod/src/v4/classic/schemas.ts
+++ b/packages/zod/src/v4/classic/schemas.ts
@@ -1401,7 +1401,7 @@ export function set<Value extends core.SomeType>(
 
 // ZodEnum
 export interface ZodEnum<
-  /* @ts-ignore Cast variance */
+  /** @ts-ignore Cast variance */
   out T extends util.EnumLike = util.EnumLike,
 > extends _ZodType<core.$ZodEnumInternals<T>>,
     core.$ZodEnum<T> {
@@ -1510,7 +1510,7 @@ export const ZodLiteral: core.$constructor<ZodLiteral> = /*@__PURE__*/ core.$con
   });
 });
 
-export function literal<const T extends Array<util.Literal>>(
+export function literal<const T extends ReadonlyArray<util.Literal>>(
   value: T,
   params?: string | core.$ZodLiteralParams
 ): ZodLiteral<T[number]>;
@@ -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/core/checks.ts b/packages/zod/src/v4/core/checks.ts
index c0708c0f..3aceb60c 100644
--- a/packages/zod/src/v4/core/checks.ts
+++ b/packages/zod/src/v4/core/checks.ts
@@ -51,7 +51,7 @@ export interface $ZodCheckLessThanDef extends $ZodCheckDef {
 
 export interface $ZodCheckLessThanInternals<T extends util.Numeric = util.Numeric> extends $ZodCheckInternals<T> {
   def: $ZodCheckLessThanDef;
-  issc: errors.$ZodIssueTooSmall<T>;
+  issc: errors.$ZodIssueTooBig<T>;
 }
 
 const numericOriginMap = {
diff --git a/packages/zod/src/v4/core/registries.ts b/packages/zod/src/v4/core/registries.ts
index 5e67b7f6..870ad97f 100644
--- a/packages/zod/src/v4/core/registries.ts
+++ b/packages/zod/src/v4/core/registries.ts
@@ -12,10 +12,16 @@ export type $replace<Meta, S extends $ZodType> = Meta extends $output
     ? core.input<S>
     : Meta extends (infer M)[]
       ? $replace<M, S>[]
-      : // handle objecs
-        Meta extends object
-        ? { [K in keyof Meta]: $replace<Meta[K], S> }
-        : Meta;
+      : Meta extends (...args: infer P) => infer R
+        ? (
+            ...args: {
+              [K in keyof P]: $replace<P[K], S>; // tuple
+            }
+          ) => $replace<R, S>
+        : // handle objects
+          Meta extends object
+          ? { [K in keyof Meta]: $replace<Meta[K], S> }
+          : Meta;
 
 type MetadataType = Record<string, unknown> | undefined;
 export class $ZodRegistry<Meta extends MetadataType = MetadataType, Schema extends $ZodType = $ZodType> {
diff --git a/packages/zod/src/v4/mini/schemas.ts b/packages/zod/src/v4/mini/schemas.ts
index 71344243..dfef934e 100644
--- a/packages/zod/src/v4/mini/schemas.ts
+++ b/packages/zod/src/v4/mini/schemas.ts
@@ -1098,7 +1098,7 @@ export const ZodMiniLiteral: core.$constructor<ZodMiniLiteral> = /*@__PURE__*/ c
   }
 );
 
-export function literal<const T extends Array<util.Literal>>(
+export function literal<const T extends ReadonlyArray<util.Literal>>(
   value: T,
   params?: string | core.$ZodLiteralParams
 ): ZodMiniLiteral<T[number]>;
@@ -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 004fea8f..41eb006b 100644
--- a/play.ts
+++ b/play.ts
@@ -1,15 +1,3 @@
 import { z } from "zod/v4";
 
-export interface SortItem<T extends string> {
-  key: T;
-  order: string;
-}
-
-export const createSortItemSchema = <T extends z.ZodType<string>>(sortKeySchema: T) =>
-  z.object({
-    key: sortKeySchema,
-    order: z.string(),
-  });
-
-const error = <T extends z.ZodType<string>>(sortKeySchema: T, defaultSortBy: SortItem<z.output<T>>[] = []) =>
-  createSortItemSchema(sortKeySchema).array().default(defaultSortBy);
+z;


Test patch:
diff --git a/packages/zod/src/v4/classic/tests/literal.test.ts b/packages/zod/src/v4/classic/tests/literal.test.ts
index e769dcd8..78d1c088 100644
--- a/packages/zod/src/v4/classic/tests/literal.test.ts
+++ b/packages/zod/src/v4/classic/tests/literal.test.ts
@@ -85,3 +85,8 @@ test(".value getter", () => {
   expect(z.literal("tuna").value).toEqual("tuna");
   expect(() => z.literal([1, 2, 3]).value).toThrow();
 });
+
+test("readonly", () => {
+  const a = ["asdf"] as const;
+  z.literal(a);
+});
diff --git a/packages/zod/src/v4/classic/tests/number.test.ts b/packages/zod/src/v4/classic/tests/number.test.ts
index 937d044c..a6084474 100644
--- a/packages/zod/src/v4/classic/tests/number.test.ts
+++ b/packages/zod/src/v4/classic/tests/number.test.ts
@@ -240,3 +240,8 @@ test("string format methods", () => {
   expect(a.parse(6)).toEqual(6);
   expect(() => a.parse(1)).toThrow();
 });
+
+test("error customization", () => {
+  z.number().gte(5, { error: (iss) => "Min: " + iss.minimum.valueOf() });
+  z.number().lte(5, { error: (iss) => "Max: " + iss.maximum.valueOf() });
+});
diff --git a/packages/zod/src/v4/classic/tests/registries.test.ts b/packages/zod/src/v4/classic/tests/registries.test.ts
index 6e4a9cbd..23209ad7 100644
--- a/packages/zod/src/v4/classic/tests/registries.test.ts
+++ b/packages/zod/src/v4/classic/tests/registries.test.ts
@@ -163,3 +163,33 @@ test("loose examples", () => {
     examples: ["example"],
   });
 });
+
+test("function meta witout replacement", () => {
+  const myReg = z.registry<{
+    defaulter: (arg: string, test: boolean) => number;
+  }>();
+
+  const mySchema = z.date();
+  myReg.add(mySchema, {
+    defaulter: (arg, _test) => {
+      return arg.length;
+    },
+  });
+
+  expect(myReg.get(mySchema)!.defaulter("hello", true)).toEqual(5);
+});
+
+test("function meta with replacement", () => {
+  const myReg = z.registry<{
+    defaulter: (arg: z.$input, test: boolean) => z.$output;
+  }>();
+
+  const mySchema = z.string().transform((val) => val.length);
+  myReg.add(mySchema, {
+    defaulter: (arg, _test) => {
+      return arg.length;
+    },
+  });
+
+  expect(myReg.get(mySchema)!.defaulter("hello", true)).toEqual(5);
+});
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 });
 });
 
diff --git a/packages/zod/src/v4/mini/tests/index.test.ts b/packages/zod/src/v4/mini/tests/index.test.ts
index 647264ac..75638656 100644
--- a/packages/zod/src/v4/mini/tests/index.test.ts
+++ b/packages/zod/src/v4/mini/tests/index.test.ts
@@ -427,6 +427,8 @@ test("z.literal", () => {
   expect(z.parse(a, "hello")).toEqual("hello");
   expect(() => z.parse(a, "world")).toThrow();
   expect(() => z.parse(a, 123)).toThrow();
+
+  z.literal(["adf"] as const);
 });
 
 test("z.file", () => {