Repository: colinhacks/zod
Pull request #4533 (2025-05-27 10:15:41)

chore: fix comment for packages/docs/content/v4/index.mdx
<!--

Development of the next major version of Zod (`v4`) is currently underway. During this phase, only bugfixes and documentation improvements are being accepted into the `main` branch. All other PRs should target the `v4` branch. Thanks for contribting to OSS!

-->

documentation improvements:  fix comment for packages/docs/content/v4/index.mdx

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Documentation**
  - Fixed a typographical error in the Versioning section to improve clarity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
deleted file mode 100644
index 26476d0a..00000000
--- a/.github/pull_request_template.md
+++ /dev/null
@@ -1,5 +0,0 @@
-<!--
-
-Development of the next major version of Zod (`v4`) is currently underway. During this phase, only bugfixes and documentation improvements are being accepted into the `main` branch. All other PRs should target the `v4` branch. Thanks for contribting to OSS!
-
--->
diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml
deleted file mode 100644
index 16e94a99..00000000
--- a/.github/workflows/release-canary.yml
+++ /dev/null
@@ -1,57 +0,0 @@
-# .github/release.yml
-
-name: Release on npm (canary)
-on:
-  push:
-    branches:
-      - main
-    paths:
-      - "packages/zod/**"
-      - ".github/workflows/release-canary.yml"
-
-jobs:
-  build_and_publish:
-    runs-on: ubuntu-latest
-    permissions:
-      contents: write
-      pull-requests: read
-      id-token: write
-    steps:
-      - uses: actions/checkout@v4
-        with:
-          fetch-depth: 0
-
-      - name: Set up Node
-        uses: actions/setup-node@v4
-        with:
-          node-version: 20
-      
-      - name: Install pnpm
-        uses: pnpm/action-setup@v4
-
-      - name: Install dependencies
-        run: |
-          pnpm install
-
-      - name: Build and test
-        working-directory: ./packages/zod
-        run: |
-          pnpm run test
-          pnpm build
-
-      - name: Set version
-        working-directory: ./packages/zod
-        run: |
-          npm --no-git-tag-version version minor
-          npm --no-git-tag-version version $(npm pkg get version | sed 's/"//g')-canary.$(date +'%Y%m%dT%H%M%S')
-
-      - id: publish
-        name: Publish to NPM
-        uses: JS-DevTools/npm-publish@v3
-        with:
-          token: ${{ secrets.NPM_TOKEN }}
-          dry-run: false
-          provenance: true
-          tag: canary
-          package: ./packages/zod
-          
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8af73fc9..82f2b1ef 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,6 +7,7 @@ on:
       - main
     paths:
       - "packages/zod/package.json"
+      - "packages/zod/src/**"
       - ".github/workflows/release.yml"
 
 jobs:
@@ -53,10 +54,27 @@ jobs:
         run: |
           echo "Published ${{ steps.publish.outputs.type }} version: ${{ steps.publish.outputs.version }}"
 
-      - name: Publish skipped
+      - name: Set canary version
+        if: ${{ !steps.publish.outputs.type }}
+        working-directory: ./packages/zod
+        run: |
+          npm --no-git-tag-version version minor
+          npm --no-git-tag-version version $(npm pkg get version | sed 's/"//g')-canary.$(date +'%Y%m%dT%H%M%S')
+
+      - name: Publish canary
+        if: ${{ !steps.publish.outputs.type }}
+        uses: JS-DevTools/npm-publish@v3
+        with:
+          token: ${{ secrets.NPM_TOKEN }}
+          dry-run: false
+          provenance: true
+          tag: canary
+          package: ./packages/zod
+      
+      - name: Done with canary
         if: ${{ !steps.publish.outputs.type }}
         run: |
-          echo "Version in package.json has not changed. Skipping."
+          echo "Canary published."
           exit 0
      
       - name: Configure changelog
diff --git a/packages/docs/content/v4/index.mdx b/packages/docs/content/v4/index.mdx
index 65d91819..e5491bfb 100644
--- a/packages/docs/content/v4/index.mdx
+++ b/packages/docs/content/v4/index.mdx
@@ -14,7 +14,7 @@ Huge thanks to [Clerk](https://go.clerk.com/zod-clerk), who supported my work on
 
 ## Versioning 
 
-To simplify the migration process both for users and Zod's ecosystem of associated libraries, Zod 4 will initially published alongside Zod 3 as part of the `zod@3.25` release. Despite the version number, it it considered stable and ready for production use.
+To simplify the migration process both for users and Zod's ecosystem of associated libraries, Zod 4 will initially published alongside Zod 3 as part of the `zod@3.25` release. Despite the version number, it is considered stable and ready for production use.
 
 To upgrade: 
 
diff --git a/packages/zod/package.json b/packages/zod/package.json
index 248e0275..bada2cf4 100644
--- a/packages/zod/package.json
+++ b/packages/zod/package.json
@@ -1,6 +1,6 @@
 {
   "name": "zod",
-  "version": "3.25.30",
+  "version": "3.25.32",
   "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 2746c004..19a0452d 100644
--- a/packages/zod/src/v4/classic/schemas.ts
+++ b/packages/zod/src/v4/classic/schemas.ts
@@ -1071,10 +1071,13 @@ export interface ZodObject<
   ): ZodObject<util.Extend<Shape, U>, Config>;
 
   /**
-   * @deprecated Use destructuring to merge the shapes:
+   * @deprecated Use spread syntax and the `.shape` property to combine two object schemas:
    *
    * ```ts
-   * z.object({
+   * const A = z.object({ a: z.string() });
+   * const B = z.object({ b: z.number() });
+   *
+   * const C = z.object({
    *    ...A.shape,
    *    ...B.shape
    * });
@@ -1153,12 +1156,10 @@ export function object<T extends core.$ZodLooseShape = Partial<Record<never, cor
 ): ZodObject<util.Writeable<T> & {}, core.$strip> {
   const def: core.$ZodObjectDef = {
     type: "object",
-
     get shape() {
       util.assignProp(this, "shape", { ...shape });
       return this.shape;
     },
-
     ...util.normalizeParams(params),
   };
   return new ZodObject(def) as any;
diff --git a/packages/zod/src/v4/core/api.ts b/packages/zod/src/v4/core/api.ts
index cafe9d44..f6787333 100644
--- a/packages/zod/src/v4/core/api.ts
+++ b/packages/zod/src/v4/core/api.ts
@@ -1476,9 +1476,10 @@ export function _stringbool(
   },
   _params?: string | $ZodStringBoolParams
 ): schemas.$ZodPipe<schemas.$ZodUnknown, schemas.$ZodBoolean<boolean>> {
-  const params = util.normalizeParams(_params);
-  const trueValues = new Set(params?.truthy ?? ["true", "1", "yes", "on", "y", "enabled"]);
-  const falseValues = new Set(params?.falsy ?? ["false", "0", "no", "off", "n", "disabled"]);
+  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"]);
 
   const _Pipe = Classes.Pipe ?? schemas.$ZodPipe;
   const _Boolean = Classes.Boolean ?? schemas.$ZodBoolean;
@@ -1492,7 +1493,7 @@ export function _stringbool(
           check: (ctx: any) => {
             if (typeof ctx.value === "string") {
               let data: string = ctx.value;
-              if (params?.case !== "sensitive") data = data.toLowerCase();
+              if (_case !== "sensitive") data = data.toLowerCase();
               if (trueValues.has(data)) {
                 ctx.value = true;
               } else if (falseValues.has(data)) {
@@ -1521,6 +1522,7 @@ export function _stringbool(
         },
       },
     ],
+    error,
   });
 
   return new _Pipe({
@@ -1528,6 +1530,8 @@ export function _stringbool(
     in: inst,
     out: new _Boolean({
       type: "boolean",
+      error,
     }),
+    error,
   }) as any;
 }
diff --git a/packages/zod/src/v4/core/checks.ts b/packages/zod/src/v4/core/checks.ts
index f617cce9..867bdfad 100644
--- a/packages/zod/src/v4/core/checks.ts
+++ b/packages/zod/src/v4/core/checks.ts
@@ -787,8 +787,12 @@ export const $ZodCheckStringFormat: core.$constructor<$ZodCheckStringFormat> = /
     $ZodCheck.init(inst, def);
 
     inst._zod.onattach.push((inst) => {
-      inst._zod.bag.format = def.format;
-      if (def.pattern) inst._zod.bag.pattern = def.pattern;
+      const bag = inst._zod.bag as schemas.$ZodStringInternals<unknown>["bag"];
+      bag.format = def.format;
+      if (def.pattern) {
+        bag.patterns ??= new Set();
+        bag.patterns.add(def.pattern);
+      }
     });
 
     inst._zod.check ??= (payload) => {
@@ -951,7 +955,9 @@ export const $ZodCheckIncludes: core.$constructor<$ZodCheckIncludes> = /*@__PURE
     const pattern = new RegExp(util.escapeRegex(def.includes));
     def.pattern = pattern;
     inst._zod.onattach.push((inst) => {
-      inst._zod.bag.pattern = pattern;
+      const bag = inst._zod.bag as schemas.$ZodStringInternals<unknown>["bag"];
+      bag.patterns ??= new Set();
+      bag.patterns.add(pattern);
     });
 
     inst._zod.check = (payload) => {
@@ -993,7 +999,9 @@ export const $ZodCheckStartsWith: core.$constructor<$ZodCheckStartsWith> = /*@__
     const pattern = new RegExp(`^${util.escapeRegex(def.prefix)}.*`);
     def.pattern ??= pattern;
     inst._zod.onattach.push((inst) => {
-      inst._zod.bag.pattern = pattern;
+      const bag = inst._zod.bag as schemas.$ZodStringInternals<unknown>["bag"];
+      bag.patterns ??= new Set();
+      bag.patterns.add(pattern);
     });
 
     inst._zod.check = (payload) => {
@@ -1035,7 +1043,9 @@ export const $ZodCheckEndsWith: core.$constructor<$ZodCheckEndsWith> = /*@__PURE
     const pattern = new RegExp(`.*${util.escapeRegex(def.suffix)}$`);
     def.pattern ??= pattern;
     inst._zod.onattach.push((inst) => {
-      inst._zod.bag.pattern = new RegExp(`.*${util.escapeRegex(def.suffix)}$`);
+      const bag = inst._zod.bag as schemas.$ZodStringInternals<unknown>["bag"];
+      bag.patterns ??= new Set();
+      bag.patterns.add(pattern);
     });
 
     inst._zod.check = (payload) => {
diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts
index b5103aff..0f4172d7 100644
--- a/packages/zod/src/v4/core/schemas.ts
+++ b/packages/zod/src/v4/core/schemas.ts
@@ -289,8 +289,9 @@ export interface $ZodStringInternals<Input> extends $ZodTypeInternals<string, In
   bag: util.LoosePartial<{
     minimum: number;
     maximum: number;
-    pattern: RegExp;
+    patterns: Set<RegExp>;
     format: string;
+    contentEncoding: string;
   }>;
 }
 
@@ -300,7 +301,7 @@ export interface $ZodString<Input = unknown> extends $ZodType {
 
 export const $ZodString: core.$constructor<$ZodString> = /*@__PURE__*/ core.$constructor("$ZodString", (inst, def) => {
   $ZodType.init(inst, def);
-  inst._zod.pattern = inst?._zod.bag?.pattern ?? regexes.string(inst._zod.bag);
+  inst._zod.pattern = [...(inst?._zod.bag?.patterns ?? [])].pop() ?? regexes.string(inst._zod.bag);
   inst._zod.parse = (payload, _) => {
     if (def.coerce)
       try {
@@ -675,7 +676,8 @@ export const $ZodIPv4: core.$constructor<$ZodIPv4> = /*@__PURE__*/ core.$constru
   def.pattern ??= regexes.ipv4;
   $ZodStringFormat.init(inst, def);
   inst._zod.onattach.push((inst) => {
-    inst._zod.bag.format = `ipv4`;
+    const bag = inst._zod.bag as $ZodStringInternals<unknown>["bag"];
+    bag.format = `ipv4`;
   });
 });
 
@@ -698,7 +700,8 @@ export const $ZodIPv6: core.$constructor<$ZodIPv6> = /*@__PURE__*/ core.$constru
   $ZodStringFormat.init(inst, def);
 
   inst._zod.onattach.push((inst) => {
-    inst._zod.bag.format = `ipv6`;
+    const bag = inst._zod.bag as $ZodStringInternals<unknown>["bag"];
+    bag.format = `ipv6`;
   });
 
   inst._zod.check = (payload) => {
@@ -1526,7 +1529,6 @@ export interface $ZodObjectInternals<
   config: Config;
   isst: errors.$ZodIssueInvalidType | errors.$ZodIssueUnrecognizedKeys;
   disc: util.DiscriminatorMap;
-
   // special keys only used for objects
   // not defined on $ZodTypeInternals (base interface) because it breaks cyclical inference
   // the z.infer<> util checks for these first when extracting inferred type
@@ -1624,6 +1626,11 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con
 
   const _normalized = util.cached(() => {
     const keys = Object.keys(def.shape);
+    for (const k of keys) {
+      if (!(def.shape[k] instanceof $ZodType)) {
+        throw new Error(`Invalid element at key "${k}": expected a Zod schema`);
+      }
+    }
     const okeys = util.optionalKeys(def.shape);
 
     return {
@@ -1769,7 +1776,7 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con
         // }
 
         const r = el._zod.run({ value: input[key], issues: [] }, ctx);
-        const isOptional = el._zod.optin === "optional";
+        const isOptional = el._zod.optin === "optional" && el._zod.optout === "optional";
 
         if (r instanceof Promise) {
           proms.push(
diff --git a/packages/zod/src/v4/core/to-json-schema.ts b/packages/zod/src/v4/core/to-json-schema.ts
index 060298ac..0d25ca04 100644
--- a/packages/zod/src/v4/core/to-json-schema.ts
+++ b/packages/zod/src/v4/core/to-json-schema.ts
@@ -48,11 +48,12 @@ interface EmitParams {
     | undefined;
 }
 
-const formatMap: Partial<Record<checks.$ZodStringFormats, string>> = {
+const formatMap: Partial<Record<checks.$ZodStringFormats, string | undefined>> = {
   guid: "uuid",
   url: "uri",
   datetime: "date-time",
   json_string: "json-string",
+  regex: "", // do not set
 };
 
 interface Seen {
@@ -149,23 +150,28 @@ export class JSONSchemaGenerator {
         case "string": {
           const json: JSONSchema.StringSchema = _json as any;
           json.type = "string";
-          const { minimum, maximum, format, pattern, contentEncoding } = schema._zod.bag as {
-            minimum?: number;
-            maximum?: number;
-            format?: checks.$ZodStringFormats;
-            pattern?: RegExp;
-            contentEncoding?: string;
-          };
+          const { minimum, maximum, format, patterns, contentEncoding } = schema._zod
+            .bag as schemas.$ZodStringInternals<unknown>["bag"];
           if (typeof minimum === "number") json.minLength = minimum;
           if (typeof maximum === "number") json.maxLength = maximum;
           // custom pattern overrides format
           if (format) {
-            json.format = formatMap[format] ?? format;
-          }
-          if (pattern) {
-            json.pattern = pattern.source;
+            json.format = formatMap[format as checks.$ZodStringFormats] ?? format;
+            if (json.format === "") delete json.format; // empty format is not valid
           }
           if (contentEncoding) json.contentEncoding = contentEncoding;
+          if (patterns && patterns.size > 0) {
+            const regexes = [...patterns];
+            if (regexes.length === 1) json.pattern = regexes[0].source;
+            else if (regexes.length > 1) {
+              result.schema.allOf = [
+                ...regexes.map((regex) => ({
+                  ...(this.target === "draft-7" ? { type: "string" } : {}),
+                  pattern: regex.source,
+                })),
+              ];
+            }
+          }
 
           break;
         }
@@ -703,7 +709,16 @@ export class JSONSchemaGenerator {
       flattenRef(entry[0], { target: this.target });
     }
 
-    const result = { ...root.def };
+    const result: JSONSchema.BaseSchema = {};
+    if (this.target === "draft-2020-12") {
+      result.$schema = "https://json-schema.org/draft/2020-12/schema";
+    } else if (this.target === "draft-7") {
+      result.$schema = "http://json-schema.org/draft-07/schema#";
+    } else {
+      console.warn(`Invalid target: ${this.target}`);
+    }
+
+    Object.assign(result, root.def);
 
     const defs: JSONSchema.BaseSchema["$defs"] = params.external?.defs ?? {};
     for (const entry of this.seen.entries()) {
@@ -722,14 +737,6 @@ export class JSONSchemaGenerator {
       }
     }
 
-    if (this.target === "draft-2020-12") {
-      result.$schema = "https://json-schema.org/draft/2020-12/schema";
-    } else if (this.target === "draft-7") {
-      result.$schema = "http://json-schema.org/draft-07/schema#";
-    } else {
-      console.warn(`Invalid target: ${this.target}`);
-    }
-
     try {
       // this "finalizes" this schema and ensures all cycles are removed
       // each call to .emit() is functionally independent
diff --git a/packages/zod/src/v4/core/util.ts b/packages/zod/src/v4/core/util.ts
index 0537bfc4..9431471e 100644
--- a/packages/zod/src/v4/core/util.ts
+++ b/packages/zod/src/v4/core/util.ts
@@ -318,7 +318,7 @@ export function esc(str: string): string {
 }
 
 export function isObject(data: any): data is Record<PropertyKey, unknown> {
-  return typeof data === "object" && data !== null;
+  return typeof data === "object" && data !== null && !Array.isArray(data);
 }
 
 export const allowsEval: { value: boolean } = cached(() => {
@@ -489,7 +489,7 @@ export function stringifyPrimitive(value: any): string {
 
 export function optionalKeys(shape: schemas.$ZodShape): string[] {
   return Object.keys(shape).filter((k) => {
-    return shape[k]._zod.optin === "optional";
+    return shape[k]._zod.optin === "optional" && shape[k]._zod.optout === "optional";
   });
 }
 
diff --git a/play.ts b/play.ts
index fcd8efad..a565b160 100644
--- a/play.ts
+++ b/play.ts
@@ -1,8 +1,3 @@
-import * as z from "zod/v4-mini";
+import { z } from "zod/v4";
 
-z;
-
-const fn = (value: string) => Uint8Array.from(Buffer.from(value, "base64"));
-
-// Argument of type 'ZodCustom<Uint8Array<ArrayBuffer>, unknown>' is not assignable to parameter of type '$ZodType<any, Uint8Array<ArrayBuffer>>'.
-z.pipe(z.pipe(z.string(), z.transform(fn)), z.instanceof(Uint8Array));
+console.dir(z.string(), { depth: null });


Test patch:
diff --git a/packages/zod/src/v4/classic/tests/default.test.ts b/packages/zod/src/v4/classic/tests/default.test.ts
index 56b85f55..28015711 100644
--- a/packages/zod/src/v4/classic/tests/default.test.ts
+++ b/packages/zod/src/v4/classic/tests/default.test.ts
@@ -112,3 +112,183 @@ test("object optionality", () => {
     hi: "hi",
   });
 });
+
+test("nested prefault/default", () => {
+  const a = z
+    .string()
+    .default("a")
+    .refine((val) => val.startsWith("a"));
+  const b = z
+    .string()
+    .refine((val) => val.startsWith("b"))
+    .default("b");
+  const c = z
+    .string()
+    .prefault("c")
+    .refine((val) => val.startsWith("c"));
+  const d = z
+    .string()
+    .refine((val) => val.startsWith("d"))
+    .prefault("d");
+
+  const obj = z.object({
+    a,
+    b,
+    c,
+    d,
+  });
+
+  expect(obj.safeParse({ a: "a1", b: "b1", c: "c1", d: "d1" })).toMatchInlineSnapshot(`
+    {
+      "data": {
+        "a": "a1",
+        "b": "b1",
+        "c": "c1",
+        "d": "d1",
+      },
+      "success": true,
+    }
+  `);
+
+  expect(obj.safeParse({ a: "f", b: "f", c: "f", d: "f" })).toMatchInlineSnapshot(`
+    {
+      "error": [ZodError: [
+      {
+        "code": "custom",
+        "path": [
+          "a"
+        ],
+        "message": "Invalid input"
+      },
+      {
+        "code": "custom",
+        "path": [
+          "b"
+        ],
+        "message": "Invalid input"
+      },
+      {
+        "code": "custom",
+        "path": [
+          "c"
+        ],
+        "message": "Invalid input"
+      },
+      {
+        "code": "custom",
+        "path": [
+          "d"
+        ],
+        "message": "Invalid input"
+      }
+    ]],
+      "success": false,
+    }
+  `);
+
+  expect(obj.safeParse({})).toMatchInlineSnapshot(`
+    {
+      "data": {
+        "a": "a",
+        "b": "b",
+        "c": "c",
+        "d": "d",
+      },
+      "success": true,
+    }
+  `);
+
+  expect(obj.safeParse({ a: undefined, b: undefined, c: undefined, d: undefined })).toMatchInlineSnapshot(`
+    {
+      "data": {
+        "a": "a",
+        "b": "b",
+        "c": "c",
+        "d": "d",
+      },
+      "success": true,
+    }
+  `);
+
+  const obj2 = z.object({
+    a: a.optional(),
+    b: b.optional(),
+    c: c.optional(),
+    d: d.optional(),
+  });
+  expect(obj2.safeParse({ a: undefined, b: undefined, c: undefined, d: undefined })).toMatchInlineSnapshot(`
+    {
+      "data": {
+        "a": undefined,
+        "b": undefined,
+        "c": undefined,
+        "d": undefined,
+      },
+      "success": true,
+    }
+  `);
+
+  expect(a.parse(undefined)).toBe("a");
+  expect(b.parse(undefined)).toBe("b");
+  expect(c.parse(undefined)).toBe("c");
+  expect(d.parse(undefined)).toBe("d");
+});
+
+test("failing default", () => {
+  const a = z
+    .string()
+    .default("z")
+    .refine((val) => val.startsWith("a"));
+  const b = z
+    .string()
+    .refine((val) => val.startsWith("b"))
+    .default("z");
+  const c = z
+    .string()
+    .prefault("z")
+    .refine((val) => val.startsWith("c"));
+  const d = z
+    .string()
+    .refine((val) => val.startsWith("d"))
+    .prefault("z");
+
+  const obj = z.object({
+    a,
+    b,
+    c,
+    d,
+  });
+
+  expect(
+    obj.safeParse({
+      a: undefined,
+      b: undefined,
+      c: undefined,
+      d: undefined,
+    }).error!.issues
+  ).toMatchInlineSnapshot(`
+    [
+      {
+        "code": "custom",
+        "message": "Invalid input",
+        "path": [
+          "a",
+        ],
+      },
+      {
+        "code": "custom",
+        "message": "Invalid input",
+        "path": [
+          "c",
+        ],
+      },
+      {
+        "code": "custom",
+        "message": "Invalid input",
+        "path": [
+          "d",
+        ],
+      },
+    ]
+  `);
+});
diff --git a/packages/zod/src/v4/classic/tests/index.test.ts b/packages/zod/src/v4/classic/tests/index.test.ts
index 559a2d2b..5ded8c49 100644
--- a/packages/zod/src/v4/classic/tests/index.test.ts
+++ b/packages/zod/src/v4/classic/tests/index.test.ts
@@ -835,48 +835,6 @@ test("z.json", () => {
   expect(() => z.parse(a, { a: undefined })).toThrow();
 });
 
-test("z.stringbool", () => {
-  const a = z.stringbool();
-
-  expect(z.parse(a, "true")).toEqual(true);
-  expect(z.parse(a, "yes")).toEqual(true);
-  expect(z.parse(a, "1")).toEqual(true);
-  expect(z.parse(a, "on")).toEqual(true);
-  expect(z.parse(a, "y")).toEqual(true);
-  expect(z.parse(a, "enabled")).toEqual(true);
-  expect(z.parse(a, "TRUE")).toEqual(true);
-
-  expect(z.parse(a, "false")).toEqual(false);
-  expect(z.parse(a, "no")).toEqual(false);
-  expect(z.parse(a, "0")).toEqual(false);
-  expect(z.parse(a, "off")).toEqual(false);
-  expect(z.parse(a, "n")).toEqual(false);
-  expect(z.parse(a, "disabled")).toEqual(false);
-  expect(z.parse(a, "FALSE")).toEqual(false);
-
-  expect(z.safeParse(a, "other")).toMatchObject({ success: false });
-  expect(z.safeParse(a, "")).toMatchObject({ success: false });
-  expect(z.safeParse(a, undefined)).toMatchObject({ success: false });
-  expect(z.safeParse(a, {})).toMatchObject({ success: false });
-  expect(z.safeParse(a, true)).toMatchObject({ success: false });
-  expect(z.safeParse(a, false)).toMatchObject({ success: false });
-
-  const b = z.stringbool({
-    truthy: ["y"],
-    falsy: ["n"],
-  });
-  expect(z.parse(b, "y")).toEqual(true);
-  expect(z.parse(b, "n")).toEqual(false);
-  expect(z.safeParse(b, "true")).toMatchObject({ success: false });
-  expect(z.safeParse(b, "false")).toMatchObject({ success: false });
-
-  const c = z.stringbool({
-    case: "sensitive",
-  });
-  expect(z.parse(c, "true")).toEqual(true);
-  expect(z.safeParse(c, "TRUE")).toMatchObject({ success: false });
-});
-
 // promise
 test("z.promise", async () => {
   const a = z.promise(z.string());
diff --git a/packages/zod/src/v4/classic/tests/json-schema.test.ts b/packages/zod/src/v4/classic/tests/json-schema.test.ts
index a6efe894..d014ddf2 100644
--- a/packages/zod/src/v4/classic/tests/json-schema.test.ts
+++ b/packages/zod/src/v4/classic/tests/json-schema.test.ts
@@ -361,13 +361,66 @@ describe("toJSONSchema", () => {
     expect(z.toJSONSchema(z.string().regex(/asdf/))).toMatchInlineSnapshot(`
       {
         "$schema": "https://json-schema.org/draft/2020-12/schema",
-        "format": "regex",
         "pattern": "asdf",
         "type": "string",
       }
     `);
   });
 
+  test("string patterns", () => {
+    expect(
+      z.toJSONSchema(z.string().startsWith("hello").includes("cruel").endsWith("world").regex(/stuff/))
+    ).toMatchInlineSnapshot(`
+      {
+        "$schema": "https://json-schema.org/draft/2020-12/schema",
+        "allOf": [
+          {
+            "pattern": "^hello.*",
+          },
+          {
+            "pattern": "cruel",
+          },
+          {
+            "pattern": ".*world$",
+          },
+          {
+            "pattern": "stuff",
+          },
+        ],
+        "type": "string",
+      }
+    `);
+
+    expect(
+      z.toJSONSchema(z.string().startsWith("hello").includes("cruel").endsWith("world").regex(/stuff/), {
+        target: "draft-7",
+      })
+    ).toMatchInlineSnapshot(`
+      {
+        "$schema": "http://json-schema.org/draft-07/schema#",
+        "allOf": [
+          {
+            "pattern": "^hello.*",
+            "type": "string",
+          },
+          {
+            "pattern": "cruel",
+            "type": "string",
+          },
+          {
+            "pattern": ".*world$",
+            "type": "string",
+          },
+          {
+            "pattern": "stuff",
+            "type": "string",
+          },
+        ],
+        "type": "string",
+      }
+    `);
+  });
+
   test("number constraints", () => {
     expect(z.toJSONSchema(z.number().min(5).max(10))).toMatchInlineSnapshot(
       `
@@ -984,6 +1037,7 @@ describe("toJSONSchema", () => {
     expect(JSON.stringify(result, null, 2)).toMatchInlineSnapshot(
       `
       "{
+        "$schema": "https://json-schema.org/draft/2020-12/schema",
         "type": "object",
         "properties": {
           "id": {
@@ -997,8 +1051,7 @@ describe("toJSONSchema", () => {
           "id",
           "children"
         ],
-        "additionalProperties": false,
-        "$schema": "https://json-schema.org/draft/2020-12/schema"
+        "additionalProperties": false
       }"
     `
     );
@@ -1025,6 +1078,7 @@ describe("toJSONSchema", () => {
     expect(JSON.stringify(result, null, 2)).toMatchInlineSnapshot(
       `
       "{
+        "$schema": "https://json-schema.org/draft/2020-12/schema",
         "type": "object",
         "properties": {
           "name": {
@@ -1054,8 +1108,7 @@ describe("toJSONSchema", () => {
           "name",
           "files"
         ],
-        "additionalProperties": false,
-        "$schema": "https://json-schema.org/draft/2020-12/schema"
+        "additionalProperties": false
       }"
     `
     );
diff --git a/packages/zod/src/v4/classic/tests/object.test.ts b/packages/zod/src/v4/classic/tests/object.test.ts
index 56d2699b..29c39806 100644
--- a/packages/zod/src/v4/classic/tests/object.test.ts
+++ b/packages/zod/src/v4/classic/tests/object.test.ts
@@ -491,3 +491,15 @@ test("preserve key order", () => {
   `);
   expect(Object.keys(r1.data!)).toEqual(Object.keys(r2.data!));
 });
+
+test("empty shape", () => {
+  const a = z.object({});
+
+  a.parse({});
+  a.parse({}, { jitless: true });
+  a.parse(Object.create(null));
+  a.parse(Object.create(null), { jitless: true });
+
+  expect(() => a.parse([])).toThrow();
+  expect(() => a.parse([], { jitless: true })).toThrow();
+});
diff --git a/packages/zod/src/v4/classic/tests/stringbool.test.ts b/packages/zod/src/v4/classic/tests/stringbool.test.ts
new file mode 100644
index 00000000..f0cd31ea
--- /dev/null
+++ b/packages/zod/src/v4/classic/tests/stringbool.test.ts
@@ -0,0 +1,53 @@
+import { expect, expectTypeOf, test } from "vitest";
+import * as z from "zod/v4";
+
+test("z.stringbool", () => {
+  const a = z.stringbool();
+  type a = z.infer<typeof a>;
+  expectTypeOf<a>().toEqualTypeOf<boolean>();
+
+  expect(z.parse(a, "true")).toEqual(true);
+  expect(z.parse(a, "yes")).toEqual(true);
+  expect(z.parse(a, "1")).toEqual(true);
+  expect(z.parse(a, "on")).toEqual(true);
+  expect(z.parse(a, "y")).toEqual(true);
+  expect(z.parse(a, "enabled")).toEqual(true);
+  expect(z.parse(a, "TRUE")).toEqual(true);
+
+  expect(z.parse(a, "false")).toEqual(false);
+  expect(z.parse(a, "no")).toEqual(false);
+  expect(z.parse(a, "0")).toEqual(false);
+  expect(z.parse(a, "off")).toEqual(false);
+  expect(z.parse(a, "n")).toEqual(false);
+  expect(z.parse(a, "disabled")).toEqual(false);
+  expect(z.parse(a, "FALSE")).toEqual(false);
+
+  expect(z.safeParse(a, "other")).toMatchObject({ success: false });
+  expect(z.safeParse(a, "")).toMatchObject({ success: false });
+  expect(z.safeParse(a, undefined)).toMatchObject({ success: false });
+  expect(z.safeParse(a, {})).toMatchObject({ success: false });
+  expect(z.safeParse(a, true)).toMatchObject({ success: false });
+  expect(z.safeParse(a, false)).toMatchObject({ success: false });
+
+  const b = z.stringbool({
+    truthy: ["y"],
+    falsy: ["n"],
+  });
+  expect(z.parse(b, "y")).toEqual(true);
+  expect(z.parse(b, "n")).toEqual(false);
+  expect(z.safeParse(b, "true")).toMatchObject({ success: false });
+  expect(z.safeParse(b, "false")).toMatchObject({ success: false });
+
+  const c = z.stringbool({
+    case: "sensitive",
+  });
+  expect(z.parse(c, "true")).toEqual(true);
+  expect(z.safeParse(c, "TRUE")).toMatchObject({ success: false });
+});
+
+// test custom error messages
+test("z.stringbool with custom error messages", () => {
+  const a = z.stringbool("wrong!");
+
+  expect(() => a.parse("")).toThrowError("wrong!");
+});