Repository: colinhacks/zod
Pull request #4602 (2025-06-02 13:14:41)

v4(toJSONSchema): Add `includes` position check
Adds an extra regex check marker to includes when `position` isa added


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..8aeedb3c 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)`          |
 
@@ -308,7 +308,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 +718,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 +861,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 +950,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 +1507,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 +1766,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 +1781,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 +1801,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 +1813,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 +2441,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 +2524,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/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"];


Test patch:
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",