Repository: colinhacks/zod
Pull request #4587 (2025-06-01 04:36:12)

fix(docs): remove z.literal(Symbol)
Zod 4 (colinhacks#4074)

- Remove `z.literal(Symbol("terrific"));` from docs in api.mdx


Patch:
diff --git a/packages/docs/components/ecosystem-v3.tsx b/packages/docs/components/ecosystem-v3.tsx
index 32da01b9..a640a3a3 100644
--- a/packages/docs/components/ecosystem-v3.tsx
+++ b/packages/docs/components/ecosystem-v3.tsx
@@ -195,8 +195,8 @@ const formIntegrations: ZodResource[] = [
     slug: "schalkventer/frrm",
   },
   {
-    name: "@regle/schemas",
-    url: "https://github.com/victorgarciaesgi/regle/tree/main/packages/schemas",
+    name: "regle",
+    url: "https://github.com/victorgarciaesgi/regle",
     description: "Headless form validation library for Vue.js.",
     slug: "victorgarciaesgi/regle",
     v4: true,
diff --git a/packages/docs/components/ecosystem.tsx b/packages/docs/components/ecosystem.tsx
index aad67859..649edc10 100644
--- a/packages/docs/components/ecosystem.tsx
+++ b/packages/docs/components/ecosystem.tsx
@@ -49,8 +49,8 @@ const apiLibraries: ZodResource[] = [
 
 const formIntegrations: ZodResource[] = [
   {
-    name: "@regle/schemas",
-    url: "https://github.com/victorgarciaesgi/regle/tree/main/packages/schemas",
+    name: "regle",
+    url: "https://github.com/victorgarciaesgi/regle",
     description: "Headless form validation library for Vue.js.",
     slug: "victorgarciaesgi/regle",
   },
diff --git a/packages/docs/content/api.mdx b/packages/docs/content/api.mdx
index 651c0efa..6fc5e411 100644
--- a/packages/docs/content/api.mdx
+++ b/packages/docs/content/api.mdx
@@ -52,9 +52,9 @@ schema.parse(null);      // => "null"
 
   | Zod API                  | Coercion                   |
   |--------------------------|----------------------------|
-  | `z.coerce.string()`      | `new String(value)`        |
-  | `z.coerce.number()`      | `new Number(value)`        |
-  | `z.coerce.boolean()`     | `new Boolean(value)`       |
+  | `z.coerce.string()`      | `String(value)`            |
+  | `z.coerce.number()`      | `Number(value)`            |
+  | `z.coerce.boolean()`     | `Boolean(value)`           |
   | `z.coerce.bigint()`      | `BigInt(value)`            |
   | `z.coerce.date()`        | `new Date(value)`          |
 
@@ -89,7 +89,6 @@ const tuna = z.literal("tuna");
 const twelve = z.literal(12);
 const twobig = z.literal(2n);
 const tru = z.literal(true);
-const terrific = z.literal(Symbol("terrific"));
 ```
 
 To represent the JavaScript literals `null` and `undefined`:
@@ -308,7 +307,7 @@ schema.parse("mailto:noreply@zod.dev"); // ✅
 schema.parse("sup"); // ✅
 ```
 
-As you can see this is quite permissive. Internally this uses the `new URL()` constructor to valid inputs; this behavior may differ across platforms and runtimes but it's the mostly rigorous way to validate URIs/URLs on any given JS runtime/engine. 
+As you can see this is quite permissive. Internally this uses the `new URL()` constructor to validate inputs; this behavior may differ across platforms and runtimes but it's the mostly rigorous way to validate URIs/URLs on any given JS runtime/engine. 
 
 To validate the hostname against a specific regex:
 
@@ -718,7 +717,7 @@ strbool.parse("1")            // => true
 strbool.parse("yes")          // => true
 strbool.parse("on")           // => true
 strbool.parse("y")            // => true
-strbool.parse("enable")       // => true
+strbool.parse("enabled")      // => true
 
 strbool.parse("false");       // => false
 strbool.parse("0");           // => false
@@ -861,7 +860,7 @@ const hello = z.templateLiteral(["hello, ", z.string()]);
 // `hello, ${string}`
 
 const cssUnits = z.enum(["px", "em", "rem", "%"]);
-const css = z.templateLiteral([z.number(), cssUnits ]);
+const css = z.templateLiteral([z.number(), cssUnits]);
 // `${number}px` | `${number}em` | `${number}rem` | `${number}%`
 
 const email = z.templateLiteral([
@@ -950,7 +949,7 @@ Dog.parse({ name: "Yeller", extraKey: true });
 
 To define a *catchall schema* that will be used to validate any unrecognized keys:
 
-```
+```ts z.object
 const DogWithStrings = z.object({
   name: z.string(),
   age: z.number().optional(),
@@ -1507,10 +1506,10 @@ type c = z.infer<typeof c>; // => number
 This can be useful for intersecting two object types. 
 
 ```ts
-const Person = z.intersection({ name: z.string() });
+const Person = z.object({ name: z.string() });
 type Person = z.infer<typeof Person>;
 
-const Employee = z.intersection({ role: z.string() });
+const Employee = z.object({ role: z.string() });
 type Employee = z.infer<typeof Employee>;
 
 const EmployedPerson = z.intersection(Person, Employee);
@@ -1766,8 +1765,8 @@ By default, validation issues from checks are considered *continuable*; that is,
 <Tab value="Zod">
 ```ts
 const myString = z.string()
-  .refine((val) => val.length > 8)
-  .refine((val) => val === val.toLowerCase());
+  .refine((val) => val.length > 8, { error: "Too short!" })
+  .refine((val) => val === val.toLowerCase(), { error: "Must be lowercase" });
   
 
 const result = myString.safeParse("OH NO");
@@ -1781,8 +1780,8 @@ result.error.issues;
 <Tab value="Zod Mini">
 ```ts zod/v4-mini
 const myString = z.string().check(
-  z.refine((val) => val.length > 8),
-  z.refine((val) => val === val.toLowerCase())
+  z.refine((val) => val.length > 8, { error: "Too short!" }),
+  z.refine((val) => val === val.toLowerCase(), { error: "Must be lowercase" })
 );
 
 const result = z.safeParse(myString, "OH NO");
@@ -1801,8 +1800,8 @@ To mark a particular refinement as *non-continuable*, use the `abort` parameter.
 <Tab value="Zod">
 ```ts
 const myString = z.string()
-  .refine((val) => val.length > 8, { abort: true })
-  .refine((val) => val === val.toLowerCase());
+  .refine((val) => val.length > 8, { error: "Too short!", abort: true })
+  .refine((val) => val === val.toLowerCase(), { error: "Must be lowercase", abort: true });
 
 
 const result = myString.safeParse("OH NO");
@@ -1813,8 +1812,8 @@ result.error!.issues;
 <Tab value="Zod Mini">
 ```ts zod/v4-mini
 const myString = z.string().check(
-  z.refine((val) => val.length > 8, { abort: true }),
-  z.refine((val) => val === val.toLowerCase(), { abort: true })
+  z.refine((val) => val.length > 8, { error: "Too short!", abort: true }),
+  z.refine((val) => val === val.toLowerCase(), { error: "Must be lowercase", abort: true })
 );
 
 const result = z.safeParse(myString, "OH NO");
@@ -2441,7 +2440,7 @@ The `z.templateLiteral` API can handle any number of string literals (e.g. `"hel
 z.templateLiteral([ "hi there" ]); 
 // `hi there`
 
-z.templateLiteral([ "email: ", z.string()]); 
+z.templateLiteral([ "email: ", z.string() ]); 
 // `email: ${string}`
 
 z.templateLiteral([ "high", z.literal(5) ]); 
@@ -2524,7 +2523,7 @@ Function schemas have an `.implement()` method which accepts a function and retu
 
 ```ts
 const computeTrimmedLength = MyFunction.implement((input) => {
-  // TypeScript knows x is a string!
+  // TypeScript knows input is a string!
   return input.trim().length;
 });
 
diff --git a/packages/docs/content/v4/index.mdx b/packages/docs/content/v4/index.mdx
index bd318149..f47b6de4 100644
--- a/packages/docs/content/v4/index.mdx
+++ b/packages/docs/content/v4/index.mdx
@@ -750,7 +750,7 @@ strbool.parse("1")            // => true
 strbool.parse("yes")          // => true
 strbool.parse("on")           // => true
 strbool.parse("y")            // => true
-strbool.parse("enable")       // => true
+strbool.parse("enabled")      // => true
 
 strbool.parse("false");       // => false
 strbool.parse("0");           // => false
diff --git a/packages/zod/package.json b/packages/zod/package.json
index 28868383..a5b43086 100644
--- a/packages/zod/package.json
+++ b/packages/zod/package.json
@@ -1,6 +1,6 @@
 {
   "name": "zod",
-  "version": "3.25.45",
+  "version": "3.25.48",
   "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/core/checks.ts b/packages/zod/src/v4/core/checks.ts
index 867bdfad..8184ac32 100644
--- a/packages/zod/src/v4/core/checks.ts
+++ b/packages/zod/src/v4/core/checks.ts
@@ -952,7 +952,8 @@ export const $ZodCheckIncludes: core.$constructor<$ZodCheckIncludes> = /*@__PURE
   (inst, def) => {
     $ZodCheck.init(inst, def);
 
-    const pattern = new RegExp(util.escapeRegex(def.includes));
+    const escapedRegex = util.escapeRegex(def.includes);
+    const pattern = new RegExp(typeof def.position === "number" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);
     def.pattern = pattern;
     inst._zod.onattach.push((inst) => {
       const bag = inst._zod.bag as schemas.$ZodStringInternals<unknown>["bag"];
diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts
index 08ce92af..8c67bc3b 100644
--- a/packages/zod/src/v4/core/schemas.ts
+++ b/packages/zod/src/v4/core/schemas.ts
@@ -114,7 +114,9 @@ export interface $ZodTypeInternals<out O = unknown, out I = unknown> {
    * @default Required
    */
 
+  /** @internal */
   optin?: "optional" | undefined;
+  /** @internal */
   optout?: "optional" | undefined;
 
   /** @internal The set of literal values that will pass validation. Must be an exhaustive set. Used to determine optionality in z.record().
@@ -149,9 +151,11 @@ export interface $ZodTypeInternals<out O = unknown, out I = unknown> {
   parent?: $ZodType | undefined;
 }
 
+export type $ZodStandardSchema<T extends $ZodType> = StandardSchemaV1.Props<core.input<T>, core.output<T>>;
+
 export interface $ZodType<O = unknown, I = unknown> {
   _zod: $ZodTypeInternals<O, I>;
-  "~standard": StandardSchemaV1.Props<core.input<this>, core.output<this>>;
+  "~standard": $ZodStandardSchema<this>;
 }
 
 export const $ZodType: core.$constructor<$ZodType> = /*@__PURE__*/ core.$constructor("$ZodType", (inst, def) => {
@@ -1989,6 +1993,8 @@ export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
       const map: Map<util.Primitive, $ZodType> = new Map();
       for (const o of opts) {
         const values = o._zod.propValues[def.discriminator];
+        if (!values || values.size === 0)
+          throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(o)}"`);
         for (const v of values) {
           if (map.has(v)) {
             throw new Error(`Duplicate discriminator value "${String(v)}"`);
@@ -2319,31 +2325,31 @@ export interface $ZodRecordDef extends $ZodTypeDef {
   valueType: $ZodType;
 }
 
-type $InferZodRecordOutput<
+export type $InferZodRecordOutput<
   Key extends $ZodRecordKey = $ZodRecordKey,
   Value extends $ZodType = $ZodType,
 > = undefined extends Key["_zod"]["values"]
-  ? string extends Key["_zod"]["output"]
-    ? Record<Key["_zod"]["output"], core.output<Value>>
-    : number extends Key["_zod"]["output"]
-      ? Record<Key["_zod"]["output"], core.output<Value>>
-      : symbol extends Key["_zod"]["output"]
-        ? Record<Key["_zod"]["output"], core.output<Value>>
-        : Partial<Record<Key["_zod"]["output"], core.output<Value>>>
-  : Record<Key["_zod"]["output"], core.output<Value>>;
-
-type $InferZodRecordInput<
+  ? string extends core.output<Key>
+    ? Record<core.output<Key>, core.output<Value>>
+    : number extends core.output<Key>
+      ? Record<core.output<Key>, core.output<Value>>
+      : symbol extends core.output<Key>
+        ? Record<core.output<Key>, core.output<Value>>
+        : Partial<Record<core.output<Key>, core.output<Value>>>
+  : Record<core.output<Key>, core.output<Value>>;
+
+export type $InferZodRecordInput<
   Key extends $ZodRecordKey = $ZodRecordKey,
   Value extends $ZodType = $ZodType,
 > = undefined extends Key["_zod"]["values"]
-  ? string extends Key["_zod"]["input"]
-    ? Record<Key["_zod"]["input"], Value["_zod"]["input"]>
-    : number extends Key["_zod"]["input"]
-      ? Record<Key["_zod"]["input"], Value["_zod"]["input"]>
-      : symbol extends Key["_zod"]["input"]
-        ? Record<Key["_zod"]["input"], Value["_zod"]["input"]>
-        : Partial<Record<Key["_zod"]["input"], Value["_zod"]["input"]>>
-  : Record<Key["_zod"]["input"], Value["_zod"]["input"]>;
+  ? string extends core.input<Key>
+    ? Record<core.input<Key>, core.input<Value>>
+    : number extends core.input<Key>
+      ? Record<core.input<Key>, core.input<Value>>
+      : symbol extends core.input<Key>
+        ? Record<core.input<Key>, core.input<Value>>
+        : Partial<Record<core.input<Key>, core.input<Value>>>
+  : Record<core.input<Key>, core.input<Value>>;
 
 export interface $ZodRecordInternals<Key extends $ZodRecordKey = $ZodRecordKey, Value extends $ZodType = $ZodType>
   extends $ZodTypeInternals<$InferZodRecordOutput<Key, Value>, $InferZodRecordInput<Key, Value>> {
diff --git a/play.ts b/play.ts
index 41eb006b..583e780d 100644
--- a/play.ts
+++ b/play.ts
@@ -1,3 +1,11 @@
-import { z } from "zod/v4";
+// import { z } from "zod/v4";
 
-z;
+import type { z } from "zod/v4";
+
+export type RefinedSchema<T extends z.core.$ZodType | z.core.$ZodUnion = z.core.$ZodType | z.core.$ZodUnion> =
+  T extends z.core.$ZodUnion
+    ? RefinedUnionSchema<T> // <-- Type instantiation is excessively deep and possibly infinite.
+    : never;
+
+export type RefinedTypeSchema<T extends object> = T;
+export type RefinedUnionSchema<T extends z.core.$ZodUnion> = T;


Test patch:
diff --git a/packages/zod/src/v4/classic/tests/brand.test.ts b/packages/zod/src/v4/classic/tests/brand.test.ts
index 87e20252..54b3e0dc 100644
--- a/packages/zod/src/v4/classic/tests/brand.test.ts
+++ b/packages/zod/src/v4/classic/tests/brand.test.ts
@@ -55,3 +55,11 @@ test("$branded", () => {
 
   expectTypeOf<typeof a>().toEqualTypeOf<z.core.$ZodBranded<z.ZodString, "a">>();
 });
+
+test("branded record", () => {
+  const recordWithBrandedNumberKeys = z.record(z.string().brand("SomeBrand"), z.number());
+  type recordWithBrandedNumberKeys = z.infer<typeof recordWithBrandedNumberKeys>;
+  expectTypeOf<recordWithBrandedNumberKeys>().toEqualTypeOf<
+    Record<string & z.core.$brand<"SomeBrand">, number | undefined>
+  >();
+});
diff --git a/packages/zod/src/v4/classic/tests/to-json-schema.test.ts b/packages/zod/src/v4/classic/tests/to-json-schema.test.ts
index d8684c94..ddc5cfe0 100644
--- a/packages/zod/src/v4/classic/tests/to-json-schema.test.ts
+++ b/packages/zod/src/v4/classic/tests/to-json-schema.test.ts
@@ -365,7 +365,15 @@ describe("toJSONSchema", () => {
 
   test("string patterns", () => {
     expect(
-      z.toJSONSchema(z.string().startsWith("hello").includes("cruel").endsWith("world").regex(/stuff/))
+      z.toJSONSchema(
+        z
+          .string()
+          .startsWith("hello")
+          .includes("cruel")
+          .includes("dark", { position: 10 })
+          .endsWith("world")
+          .regex(/stuff/)
+      )
     ).toMatchInlineSnapshot(`
       {
         "$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -376,6 +384,9 @@ describe("toJSONSchema", () => {
           {
             "pattern": "cruel",
           },
+          {
+            "pattern": "^.{10}dark",
+          },
           {
             "pattern": ".*world$",
           },
@@ -388,9 +399,18 @@ describe("toJSONSchema", () => {
     `);
 
     expect(
-      z.toJSONSchema(z.string().startsWith("hello").includes("cruel").endsWith("world").regex(/stuff/), {
-        target: "draft-7",
-      })
+      z.toJSONSchema(
+        z
+          .string()
+          .startsWith("hello")
+          .includes("cruel")
+          .includes("dark", { position: 10 })
+          .endsWith("world")
+          .regex(/stuff/),
+        {
+          target: "draft-7",
+        }
+      )
     ).toMatchInlineSnapshot(`
       {
         "$schema": "http://json-schema.org/draft-07/schema#",
@@ -403,6 +423,10 @@ describe("toJSONSchema", () => {
             "pattern": "cruel",
             "type": "string",
           },
+          {
+            "pattern": "^.{10}dark",
+            "type": "string",
+          },
           {
             "pattern": ".*world$",
             "type": "string",