Repository: colinhacks/zod
Pull request #4387 (2025-05-17 04:06:12)

Simplify object generics, add test for empty objects

coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

<details open="true">
<summary>🧪 Generate Unit Tests</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with Unit Tests
- [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Commit Unit Tests in branch `v4-simplify-zodobject-params`
- [ ] <!-- {"checkboxId": "07f1e7d6-8a8e-4e23-9900-8731c2c87f58", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Post Copyable Unit Tests in Comment

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=colinhacks/zod&utm_content=4387):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai generate unit tests` to generate unit tests for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Patch:
diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts
index b2187d22..a1a0d001 100644
--- a/packages/zod/src/v4/classic/schemas.ts
+++ b/packages/zod/src/v4/classic/schemas.ts
@@ -1055,37 +1055,33 @@ export function keyof<T extends ZodObject>(schema: T): ZodLiteral<keyof T["_zod"
 }
 
 // ZodObject
+
 export interface ZodObject<
   // @ts-ignore cast variance
   out Shape extends core.$ZodShape = core.$ZodLooseShape,
-  OutExtra extends Record<string, unknown> = Record<string, unknown>,
-  InExtra extends Record<string, unknown> = Record<string, unknown>,
+  out Config extends core.$ZodObjectConfig = core.$ZodObjectConfig,
 > extends ZodType {
-  _zod: core.$ZodObjectInternals<Shape, OutExtra, InExtra>;
+  _zod: core.$ZodObjectInternals<Shape, Config>;
   shape: Shape;
 
   keyof(): ZodEnum<util.ToEnum<keyof Shape & string>>;
   /** Define a schema to validate all unrecognized keys. This overrides the existing strict/loose behavior. */
-  catchall<T extends core.$ZodType>(schema: T): ZodObject<Shape, Record<string, T["_zod"]["output"]>>;
+  catchall<T extends core.$ZodType>(schema: T): ZodObject<Shape, core.$catchall<T>>;
 
   /** @deprecated Use `z.looseObject()` or `.loose()` instead. */
-  passthrough(): ZodObject<Shape, Record<string, unknown>>;
+  passthrough(): ZodObject<Shape, core.$loose>;
   /** Consider `z.looseObject(A.shape)` instead */
-  loose(): ZodObject<Shape, Record<string, unknown>>;
+  loose(): ZodObject<Shape, core.$loose>;
 
   /** Consider `z.strictObject(A.shape)` instead */
-  strict(): ZodObject<Shape, {}>;
+  strict(): ZodObject<Shape, core.$strict>;
 
   /** This is the default behavior. This method call is likely unnecessary. */
-  strip(): ZodObject<Shape, {}>;
+  strip(): ZodObject<Shape, core.$strict>;
 
   extend<U extends core.$ZodLooseShape & Partial<Record<keyof Shape, core.$ZodType>>>(
     shape: U
-  ): ZodObject<
-    util.Extend<Shape, U>,
-    OutExtra,
-    InExtra // & B['_zod']["extra"]
-  >;
+  ): ZodObject<util.Extend<Shape, U>, Config>;
 
   /**
    * @deprecated Use destructuring to merge the shapes:
@@ -1097,24 +1093,21 @@ export interface ZodObject<
    * });
    * ```
    */
-  merge<U extends ZodObject>(
-    other: U
-  ): ZodObject<util.Extend<Shape, U["shape"]>, U["_zod"]["outextra"], U["_zod"]["inextra"]>;
+  merge<U extends ZodObject>(other: U): ZodObject<util.Extend<Shape, U["shape"]>, U["_zod"]["config"]>;
 
   pick<M extends util.Exactly<util.Mask<keyof Shape>, M>>(
     mask: M
-  ): ZodObject<util.Flatten<Pick<Shape, Extract<keyof Shape, keyof M>>>, OutExtra, InExtra>;
+  ): ZodObject<util.Flatten<Pick<Shape, Extract<keyof Shape, keyof M>>>, Config>;
 
   omit<M extends util.Exactly<util.Mask<keyof Shape>, M>>(
     mask: M
-  ): ZodObject<util.Flatten<Omit<Shape, Extract<keyof Shape, keyof M>>>, OutExtra, InExtra>;
+  ): ZodObject<util.Flatten<Omit<Shape, Extract<keyof Shape, keyof M>>>, Config>;
 
   partial(): ZodObject<
     {
       [k in keyof Shape]: ZodOptional<Shape[k]>;
     },
-    OutExtra,
-    InExtra
+    Config
   >;
   partial<M extends util.Exactly<util.Mask<keyof Shape>, M>>(
     mask: M
@@ -1122,8 +1115,7 @@ export interface ZodObject<
     {
       [k in keyof Shape]: k extends keyof M ? ZodOptional<Shape[k]> : Shape[k];
     },
-    OutExtra,
-    InExtra
+    Config
   >;
 
   // required
@@ -1131,8 +1123,7 @@ export interface ZodObject<
     {
       [k in keyof Shape]: ZodNonOptional<Shape[k]>;
     },
-    OutExtra,
-    InExtra
+    Config
   >;
   required<M extends util.Exactly<util.Mask<keyof Shape>, M>>(
     mask: M
@@ -1140,8 +1131,7 @@ export interface ZodObject<
     {
       [k in keyof Shape]: k extends keyof M ? ZodNonOptional<Shape[k]> : Shape[k];
     },
-    OutExtra,
-    InExtra
+    Config
   >;
 }
 
@@ -1173,7 +1163,7 @@ export const ZodObject: core.$constructor<ZodObject> = /*@__PURE__*/ core.$const
 export function object<T extends core.$ZodLooseShape = Partial<Record<never, core.$ZodType>>>(
   shape?: T,
   params?: string | core.$ZodObjectParams
-): ZodObject<util.Writeable<T> & {}, {}, {}> {
+): ZodObject<util.Writeable<T> & {}, core.$strip> {
   const def: core.$ZodObjectDef = {
     type: "object",
 
@@ -1192,7 +1182,7 @@ export function object<T extends core.$ZodLooseShape = Partial<Record<never, cor
 export function strictObject<T extends core.$ZodLooseShape>(
   shape: T,
   params?: string | core.$ZodObjectParams
-): ZodObject<T, {}, {}> {
+): ZodObject<T, core.$strict> {
   return new ZodObject({
     type: "object",
 
@@ -1207,10 +1197,11 @@ export function strictObject<T extends core.$ZodLooseShape>(
 }
 
 // looseObject
+
 export function looseObject<T extends core.$ZodLooseShape>(
   shape: T,
   params?: string | core.$ZodObjectParams
-): ZodObject<T, { [k: string]: unknown }, { [k: string]: unknown }> {
+): ZodObject<T, core.$loose> {
   return new ZodObject({
     type: "object",
 
diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts
index f134a07b..34acf032 100644
--- a/packages/zod/src/v4/core/schemas.ts
+++ b/packages/zod/src/v4/core/schemas.ts
@@ -1565,20 +1565,18 @@ export interface $ZodObjectDef<Shape extends $ZodShape = $ZodShape> extends $Zod
 export interface $ZodObjectInternals<
   // @ts-ignore Cast variance
   out Shape extends Readonly<$ZodShape> = Readonly<$ZodShape>,
-  out OutExtra extends Record<string, unknown> = Record<string, unknown>,
-  out InExtra extends Record<string, unknown> = Record<string, unknown>,
+  out Config extends $ZodObjectConfig = $ZodObjectConfig,
 > extends $ZodTypeInternals<any, any> {
   def: $ZodObjectDef<Shape>;
-  outextra: OutExtra;
-  inextra: InExtra;
+  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
-  "~output": $InferObjectOutput<Shape, OutExtra>;
-  "~input": $InferObjectInput<Shape, InExtra>;
+  "~output": $InferObjectOutput<Shape, Config["out"]>;
+  "~input": $InferObjectInput<Shape, Config["in"]>;
 }
 export type $ZodLooseShape = Record<string, any>;
 
@@ -1587,26 +1585,25 @@ type OptionalInSchema = { _zod: { optionality: "defaulted" | "optional" } };
 
 export type $InferObjectOutput<T extends $ZodLooseShape, Extra extends Record<string, unknown>> = string extends keyof T
   ? object
-  : keyof T extends never
+  : keyof (T & Extra) extends never
     ? Record<string, never>
     : util.Prettify<
         {
           -readonly [k in keyof T as T[k] extends OptionalOutSchema ? never : k]: core.output<T[k]>;
         } & {
           -readonly [k in keyof T as T[k] extends OptionalOutSchema ? k : never]?: core.output<T[k]>;
-        }
-      > &
-        Extra;
+        } & Extra
+      >;
 
 export type $InferObjectInput<T extends $ZodLooseShape, Extra extends Record<string, unknown>> = string extends keyof T
   ? object
-  : keyof T extends never
+  : keyof (T & Extra) extends never
     ? Record<string, never>
     : util.Prettify<
         {
-          [k in keyof T as T[k] extends OptionalInSchema ? never : k]: core.input<T[k]>;
+          -readonly [k in keyof T as T[k] extends OptionalInSchema ? never : k]: core.input<T[k]>;
         } & {
-          [k in keyof T as T[k] extends OptionalInSchema ? k : never]?: core.input<T[k]>;
+          -readonly [k in keyof T as T[k] extends OptionalInSchema ? k : never]?: core.input<T[k]>;
         } & Extra
       >;
 
@@ -1641,13 +1638,32 @@ function handleOptionalObjectResult(result: ParsePayload, final: ParsePayload, k
   }
 }
 
+export type $ZodObjectConfig = { out: Record<string, unknown>; in: Record<string, unknown> };
+
+export type $loose = {
+  out: Record<string, unknown>;
+  in: Record<string, unknown>;
+};
+export type $strict = {
+  out: {};
+  in: {};
+};
+export type $strip = {
+  out: {};
+  in: {};
+};
+export type $catchall<T extends $ZodType> = {
+  out: { [k: string]: core.output<T> };
+  in: { [k: string]: core.input<T> };
+};
 export interface $ZodObject<
   // @ts-ignore Cast variance
   out Shape extends Readonly<$ZodShape> = Readonly<$ZodShape>,
-  out OutExtra extends Record<string, unknown> = Record<string, unknown>,
-  out InExtra extends Record<string, unknown> = Record<string, unknown>,
+  out Params extends $ZodObjectConfig = $ZodObjectConfig,
+  // out OutExtra extends Record<string, unknown> = Record<string, unknown>,
+  // out InExtra extends Record<string, unknown> = Record<string, unknown>,
 > extends $ZodType {
-  _zod: $ZodObjectInternals<Shape, OutExtra, InExtra>;
+  _zod: $ZodObjectInternals<Shape, Params>;
 }
 
 export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$constructor("$ZodObject", (inst, def) => {
diff --git a/packages/zod/src/v4/mini/schemas.ts b/packages/zod/src/v4/mini/schemas.ts
index 840f72ea..ef86000d 100644
--- a/packages/zod/src/v4/mini/schemas.ts
+++ b/packages/zod/src/v4/mini/schemas.ts
@@ -694,10 +694,9 @@ export function keyof<T extends ZodMiniObject>(schema: T): ZodMiniLiteral<keyof
 export interface ZodMiniObject<
   // @ts-ignore Cast variance
   out Shape extends core.$ZodShape = core.$ZodShape,
-  OutExtra extends Record<string, unknown> = Record<string, unknown>,
-  InExtra extends Record<string, unknown> = Record<string, unknown>,
+  out Config extends core.$ZodObjectConfig = core.$ZodObjectConfig,
 > extends ZodMiniType {
-  _zod: core.$ZodObjectInternals<Shape, OutExtra, InExtra>;
+  _zod: core.$ZodObjectInternals<Shape, Config>;
   shape: Shape;
 }
 export const ZodMiniObject: core.$constructor<ZodMiniObject> = /*@__PURE__*/ core.$constructor(
@@ -710,7 +709,7 @@ export const ZodMiniObject: core.$constructor<ZodMiniObject> = /*@__PURE__*/ cor
 export function object<T extends core.$ZodLooseShape = Record<never, SomeType>>(
   shape?: T,
   params?: string | core.$ZodObjectParams
-): ZodMiniObject<T, {}, {}> {
+): ZodMiniObject<T, core.$strip> {
   const def: core.$ZodObjectDef = {
     type: "object",
     get shape() {
@@ -727,7 +726,7 @@ export function object<T extends core.$ZodLooseShape = Record<never, SomeType>>(
 export function strictObject<T extends core.$ZodLooseShape>(
   shape: T,
   params?: string | core.$ZodObjectParams
-): ZodMiniObject<T, {}, {}> {
+): ZodMiniObject<T, core.$strict> {
   return new ZodMiniObject({
     type: "object",
     // shape: shape as core.$ZodLooseShape,
@@ -748,7 +747,7 @@ export function strictObject<T extends core.$ZodLooseShape>(
 export function looseObject<T extends core.$ZodLooseShape>(
   shape: T,
   params?: string | core.$ZodObjectParams
-): ZodMiniObject<T, { [k: string]: unknown }, { [k: string]: unknown }> {
+): ZodMiniObject<T, core.$loose> {
   return new ZodMiniObject({
     type: "object",
     // shape: shape as core.$ZodLooseShape,
@@ -768,7 +767,7 @@ export function looseObject<T extends core.$ZodLooseShape>(
 export function extend<T extends ZodMiniObject, U extends core.$ZodLooseShape>(
   schema: T,
   shape: U
-): ZodMiniObject<util.Extend<T["shape"], U>, T["_zod"]["outextra"], T["_zod"]["inextra"]> {
+): ZodMiniObject<util.Extend<T["shape"], U>, T["_zod"]["config"]> {
   return util.extend(schema, shape);
 }
 
@@ -776,7 +775,7 @@ export function extend<T extends ZodMiniObject, U extends core.$ZodLooseShape>(
 export function merge<T extends ZodMiniObject, U extends ZodMiniObject>(
   a: T,
   b: U
-): ZodMiniObject<util.Extend<T["shape"], U["shape"]>, T["_zod"]["outextra"], T["_zod"]["inextra"]>;
+): ZodMiniObject<util.Extend<T["shape"], U["shape"]>, T["_zod"]["config"]>;
 export function merge(schema: ZodMiniObject, shape: any): ZodMiniObject {
   return util.extend(schema, shape);
 }
@@ -784,11 +783,7 @@ export function merge(schema: ZodMiniObject, shape: any): ZodMiniObject {
 export function pick<T extends ZodMiniObject, M extends util.Mask<keyof T["shape"]>>(
   schema: T,
   mask: M
-): ZodMiniObject<
-  util.Flatten<Pick<T["shape"], keyof T["shape"] & keyof M>>,
-  T["_zod"]["outextra"],
-  T["_zod"]["inextra"]
-> {
+): ZodMiniObject<util.Flatten<Pick<T["shape"], keyof T["shape"] & keyof M>>, T["_zod"]["config"]> {
   return util.pick(schema, mask as any);
 }
 
@@ -797,7 +792,7 @@ export function pick<T extends ZodMiniObject, M extends util.Mask<keyof T["shape
 export function omit<T extends ZodMiniObject, const M extends util.Mask<keyof T["shape"]>>(
   schema: T,
   mask: M
-): ZodMiniObject<util.Flatten<Omit<T["shape"], keyof M>>, T["_zod"]["outextra"], T["_zod"]["inextra"]> {
+): ZodMiniObject<util.Flatten<Omit<T["shape"], keyof M>>, T["_zod"]["config"]> {
   return util.omit(schema, mask);
 }
 
@@ -807,8 +802,7 @@ export function partial<T extends ZodMiniObject>(
   {
     [k in keyof T["shape"]]: ZodMiniOptional<T["shape"][k]>;
   },
-  T["_zod"]["outextra"],
-  T["_zod"]["inextra"]
+  T["_zod"]["config"]
 >;
 export function partial<T extends ZodMiniObject, M extends util.Mask<keyof T["shape"]>>(
   schema: T,
@@ -817,8 +811,7 @@ export function partial<T extends ZodMiniObject, M extends util.Mask<keyof T["sh
   {
     [k in keyof T["shape"]]: k extends keyof M ? ZodMiniOptional<T["shape"][k]> : T["shape"][k];
   },
-  T["_zod"]["outextra"],
-  T["_zod"]["inextra"]
+  T["_zod"]["config"]
 >;
 export function partial(schema: ZodMiniObject, mask?: object) {
   return util.partial(ZodMiniOptional, schema, mask);
@@ -841,8 +834,7 @@ export function required<T extends ZodMiniObject>(
   {
     [k in keyof T["shape"]]: ZodMiniNonOptional<T["shape"][k]>;
   },
-  T["_zod"]["outextra"],
-  T["_zod"]["inextra"]
+  T["_zod"]["config"]
 >;
 export function required<T extends ZodMiniObject, M extends util.Mask<keyof T["shape"]>>(
   schema: T,
@@ -854,8 +846,7 @@ export function required<T extends ZodMiniObject, M extends util.Mask<keyof T["s
       [k in keyof M & keyof T["shape"]]: ZodMiniNonOptional<T["shape"][k]>;
     }
   >,
-  T["_zod"]["outextra"],
-  T["_zod"]["inextra"]
+  T["_zod"]["config"]
 >;
 export function required(schema: ZodMiniObject, mask?: object) {
   return util.required(ZodMiniNonOptional, schema, mask);
diff --git a/play.ts b/play.ts
index 0c4bfce9..1263049d 100644
--- a/play.ts
+++ b/play.ts
@@ -2,17 +2,11 @@ import { z } from "zod/v4";
 
 z;
 
-const schema = z.object({
-  name: z.string(),
-  logLevel: z.union([z.string(), z.number()]),
-  env: z.literal(["production", "development"]),
-});
-
-const data = {
-  name: 1000,
-  logLevel: false,
-};
-
-const result = schema.safeParse(data);
-
-console.dir(z.prettifyError(result.error!), { depth: null });
+const stringWithDefault = z
+  .pipe(
+    z.transform((v) => (v === "none" ? undefined : v)),
+    z.string()
+  )
+  .catch("default");
+
+stringWithDefault.parse(undefined);


Test patch:
diff --git a/packages/zod/src/v4/classic/tests/intersection.test.ts b/packages/zod/src/v4/classic/tests/intersection.test.ts
index 76d045f1..d1538c03 100644
--- a/packages/zod/src/v4/classic/tests/intersection.test.ts
+++ b/packages/zod/src/v4/classic/tests/intersection.test.ts
@@ -21,7 +21,7 @@ test("object intersection: loose", () => {
 
   const C = z.intersection(A, B); // BaseC.merge(HasID);
   type C = z.infer<typeof C>;
-  expectTypeOf<C>().toEqualTypeOf<{ a: string } & { b: string } & Record<string, unknown>>();
+  expectTypeOf<C>().toEqualTypeOf<{ a: string; [x: string]: unknown } & { b: string }>();
   const data = { a: "foo", b: "foo", c: "extra" };
   expect(C.parse(data)).toEqual(data);
   expect(() => C.parse({ a: "foo" })).toThrow();
diff --git a/packages/zod/src/v4/classic/tests/object.test.ts b/packages/zod/src/v4/classic/tests/object.test.ts
index 3c58c6b6..0429eade 100644
--- a/packages/zod/src/v4/classic/tests/object.test.ts
+++ b/packages/zod/src/v4/classic/tests/object.test.ts
@@ -417,7 +417,7 @@ test("passthrough index signature", () => {
   expectTypeOf<a>().toEqualTypeOf<{ a: string }>();
   const b = a.passthrough();
   type b = z.infer<typeof b>;
-  expectTypeOf<b>().toEqualTypeOf<{ a: string } & { [k: string]: unknown }>();
+  expectTypeOf<b>().toEqualTypeOf<{ a: string; [k: string]: unknown }>();
 });
 
 // test("xor", () => {
@@ -454,3 +454,23 @@ test("null prototype", () => {
   obj.a = "foo";
   expect(schema.parse(obj)).toEqual({ a: "foo" });
 });
+
+test("empty objects", () => {
+  const A = z.looseObject({});
+  type Ain = z.input<typeof A>;
+  expectTypeOf<Ain>().toEqualTypeOf<Record<string, unknown>>();
+  type Aout = z.output<typeof A>;
+  expectTypeOf<Aout>().toEqualTypeOf<Record<string, unknown>>();
+
+  const B = z.object({});
+  type Bout = z.output<typeof B>;
+  expectTypeOf<Bout>().toEqualTypeOf<Record<string, never>>();
+  type Bin = z.input<typeof B>;
+  expectTypeOf<Bin>().toEqualTypeOf<Record<string, never>>();
+
+  const C = z.strictObject({});
+  type Cout = z.output<typeof C>;
+  expectTypeOf<Cout>().toEqualTypeOf<Record<string, never>>();
+  type Cin = z.input<typeof C>;
+  expectTypeOf<Cin>().toEqualTypeOf<Record<string, never>>();
+});
diff --git a/packages/zod/src/v4/classic/tests/pickomit-object.test.ts b/packages/zod/src/v4/classic/tests/pickomit.test.ts
similarity index 98%
rename from packages/zod/src/v4/classic/tests/pickomit-object.test.ts
rename to packages/zod/src/v4/classic/tests/pickomit.test.ts
index 30a0d83b..5e8819c8 100644
--- a/packages/zod/src/v4/classic/tests/pickomit-object.test.ts
+++ b/packages/zod/src/v4/classic/tests/pickomit.test.ts
@@ -93,7 +93,7 @@ test("omit - remove optional", () => {
 test("nonstrict inference", () => {
   const laxfish = fish.pick({ name: true }).catchall(z.any());
   type laxfish = z.infer<typeof laxfish>;
-  expectTypeOf<laxfish>().toEqualTypeOf<{ name: string } & { [k: string]: any }>();
+  expectTypeOf<laxfish>().toEqualTypeOf<{ name: string; [k: string]: any }>();
 });
 
 test("nonstrict parsing - pass", () => {