Repository: colinhacks/zod
Pull request #4405 (2025-05-18 23:30:28)

Implement pipe optionality. Drop `._zod.optionality` in favor of `._zod.optin` and `._zod.optout`.
Closes https://github.com/colinhacks/zod/issues/4322

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-pipe-optionality`
- [ ] <!-- {"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=4405):

- 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/docs/components/ecosystem.tsx b/packages/docs/components/ecosystem.tsx
index e963af05..4c07febe 100644
--- a/packages/docs/components/ecosystem.tsx
+++ b/packages/docs/components/ecosystem.tsx
@@ -38,7 +38,6 @@ const apiLibraries: ZodResource[] = [
   //   description: "Zod validator middleware for Hono",
   //   slug: "honojs/middleware",
   // },
-
 ];
 
 const formIntegrations: ZodResource[] = [
diff --git a/packages/zod/src/v3/helpers/partialUtil.ts b/packages/zod/src/v3/helpers/partialUtil.ts
index b9be2e9b..0eff8ff3 100644
--- a/packages/zod/src/v3/helpers/partialUtil.ts
+++ b/packages/zod/src/v3/helpers/partialUtil.ts
@@ -10,31 +10,6 @@ import type {
 } from "../types.js";
 
 export namespace partialUtil {
-  // export type DeepPartial<T extends AnyZodObject> = T extends AnyZodObject
-  //   ? ZodObject<
-  //       { [k in keyof T["_shape"]]: InternalDeepPartial<T["_shape"][k]> },
-  //       T["_unknownKeys"],
-  //       T["_catchall"]
-  //     >
-  //   : T extends ZodArray<infer Type, infer Card>
-  //   ? ZodArray<InternalDeepPartial<Type>, Card>
-  //   : ZodOptional<T>;
-
-  // {
-  //   // optional: T extends ZodOptional<ZodTypeAny> ? T : ZodOptional<T>;
-  //   // array: T extends ZodArray<infer Type> ? ZodArray<DeepPartial<Type>> : never;
-  //   object: T extends AnyZodObject
-  //     ? ZodObject<
-  //         { [k in keyof T["_shape"]]: DeepPartial<T["_shape"][k]> },
-  //         T["_unknownKeys"],
-  //         T["_catchall"]
-  //       >
-  //     : never;
-  //   rest: ReturnType<T["optional"]>; // ZodOptional<T>;
-  // }[T extends AnyZodObject
-  //   ? "object" // T extends ZodOptional<any> // ? 'optional' // :
-  //   : "rest"];
-
   export type DeepPartial<T extends ZodTypeAny> = T extends ZodObject<ZodRawShape>
     ? ZodObject<
         { [k in keyof T["shape"]]: ZodOptional<DeepPartial<T["shape"][k]>> },
@@ -56,20 +31,4 @@ export namespace partialUtil {
                 : never
               : never
             : T;
-  //  {
-  //     // optional: T extends ZodOptional<ZodTypeAny> ? T : ZodOptional<T>;
-  //     // array: T extends ZodArray<infer Type> ? ZodArray<DeepPartial<Type>> : never;
-  //     object: T extends ZodObject<infer Shape, infer Params, infer Catchall>
-  //       ? ZodOptional<
-  //           ZodObject<
-  //             { [k in keyof Shape]: DeepPartial<Shape[k]> },
-  //             Params,
-  //             Catchall
-  //           >
-  //         >
-  //       : never;
-  //     rest: ReturnType<T["optional"]>;
-  //   }[T extends ZodObject<any>
-  //     ? "object" // T extends ZodOptional<any> // ? 'optional' // :
-  //     : "rest"];
 }
diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts
index 8f0eda26..9afacb5b 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> {
   /** @internal Indicates that a schema output type should be considered optional inside objects.
    * @default Required
    */
-  optionality?: "optional" | "defaulted" | undefined;
+
+  optin?: "optional" | undefined;
+  optout?: "optional" | undefined;
 
   /** @internal A set of literal discriminators used for the fast path in discriminated unions. */
   disc: util.DiscriminatorMap | undefined;
@@ -1597,8 +1599,8 @@ export interface $ZodObjectInternals<
 }
 export type $ZodLooseShape = Record<string, any>;
 
-type OptionalOutSchema = { _zod: { optionality: "optional" } };
-type OptionalInSchema = { _zod: { optionality: "defaulted" | "optional" } };
+type OptionalOutSchema = { _zod: { optout: "optional" } };
+type OptionalInSchema = { _zod: { optin: "optional" } };
 
 export type $InferObjectOutput<T extends $ZodLooseShape, Extra extends Record<string, unknown>> = string extends keyof T
   ? object
@@ -1634,6 +1636,7 @@ function handleObjectResult(result: ParsePayload, final: ParsePayload, key: Prop
 }
 
 function handleOptionalObjectResult(result: ParsePayload, final: ParsePayload, key: PropertyKey, input: any) {
+  // console.dir({ key, result }, { depth: null });
   if (result.issues.length) {
     // validation failed against value schema
     if (input[key] === undefined) {
@@ -1687,6 +1690,7 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con
   const _normalized = util.cached(() => {
     const keys = Object.keys(def.shape);
     const okeys = util.optionalKeys(def.shape);
+    console.dir(okeys, { depth: null });
     return {
       shape: def.shape,
       keys,
@@ -1720,6 +1724,7 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con
   const generateFastpass = (shape: any) => {
     const doc = new Doc(["shape", "payload", "ctx"]);
     const { keys, optionalKeys } = _normalized.value;
+    console.dir({ keys, optionalKeys }, { depth: null });
 
     const parseStr = (key: string) => {
       const k = util.esc(key);
@@ -1740,6 +1745,7 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con
         const id = ids[key];
         doc.write(`const ${id} = ${parseStr(key)};`);
         const k = util.esc(key);
+        doc.write(`console.log(${id});`);
         doc.write(`
         if (${id}.issues.length) {
           if (input[${k}] === undefined) {
@@ -1757,7 +1763,7 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con
         } else if (${id}.value === undefined) {
           if (${k} in input) newResult[${k}] = undefined;
         } else {
-          if (${k} in input) newResult[${k}] = ${id}.value;
+          newResult[${k}] = ${id}.value;
         }
         `);
       } else {
@@ -1830,7 +1836,7 @@ export const $ZodObject: core.$constructor<$ZodObject> = /*@__PURE__*/ core.$con
         // }
 
         const r = el._zod.run({ value: input[key], issues: [] }, ctx);
-        const isOptional = el._zod.optionality === "optional";
+        const isOptional = el._zod.optin === "optional";
 
         if (r instanceof Promise) {
           proms.push(
@@ -2258,7 +2264,7 @@ type TupleInputTypeWithOptionals<T extends util.TupleItems> = T extends readonly
   ...infer Prefix extends $ZodType[],
   infer Tail extends $ZodType,
 ]
-  ? Tail["_zod"]["optionality"] extends "optional" | "defaulted"
+  ? Tail["_zod"]["optin"] extends "optional"
     ? [...TupleInputTypeWithOptionals<Prefix>, Tail["_zod"]["input"]?]
     : TupleInputTypeNoOptionals<T>
   : [];
@@ -2274,7 +2280,7 @@ type TupleOutputTypeWithOptionals<T extends util.TupleItems> = T extends readonl
   ...infer Prefix extends $ZodType[],
   infer Tail extends $ZodType,
 ]
-  ? Tail["_zod"]["optionality"] extends "optional"
+  ? Tail["_zod"]["optout"] extends "optional"
     ? [...TupleOutputTypeWithOptionals<Prefix>, core.output<Tail>?]
     : TupleOutputTypeNoOptionals<T>
   : [];
@@ -2295,7 +2301,7 @@ export interface $ZodTuple<T extends util.TupleItems = util.TupleItems, Rest ext
 export const $ZodTuple: core.$constructor<$ZodTuple> = /*@__PURE__*/ core.$constructor("$ZodTuple", (inst, def) => {
   $ZodType.init(inst, def);
   const items = def.items;
-  const optStart = items.length - [...items].reverse().findIndex((item) => item._zod.optionality !== "optional");
+  const optStart = items.length - [...items].reverse().findIndex((item) => item._zod.optin !== "optional");
 
   inst._zod.parse = (payload, ctx) => {
     const input = payload.value;
@@ -2953,7 +2959,8 @@ export interface $ZodOptionalDef<T extends $ZodType = $ZodType> extends $ZodType
 export interface $ZodOptionalInternals<T extends $ZodType = $ZodType>
   extends $ZodTypeInternals<core.output<T> | undefined, core.input<T> | undefined> {
   def: $ZodOptionalDef<T>;
-  optionality: "optional";
+  optin: "optional";
+  optout: "optional";
   isst: never;
   values: T["_zod"]["values"];
   pattern: T["_zod"]["pattern"];
@@ -2967,7 +2974,8 @@ export const $ZodOptional: core.$constructor<$ZodOptional> = /*@__PURE__*/ core.
   "$ZodOptional",
   (inst, def) => {
     $ZodType.init(inst, def);
-    inst._zod.optionality = "optional";
+    inst._zod.optin = "optional";
+    inst._zod.optout = "optional";
 
     util.defineLazy(inst._zod, "values", () => {
       console.dir("VALUES", { depth: null });
@@ -3002,7 +3010,8 @@ export interface $ZodNullableDef<T extends $ZodType = $ZodType> extends $ZodType
 export interface $ZodNullableInternals<T extends $ZodType = $ZodType>
   extends $ZodTypeInternals<core.output<T> | null, core.input<T> | null> {
   def: $ZodNullableDef<T>;
-  optionality: T["_zod"]["optionality"];
+  optin: T["_zod"]["optin"];
+  optout: T["_zod"]["optout"];
   isst: never;
   values: T["_zod"]["values"];
   pattern: T["_zod"]["pattern"];
@@ -3016,7 +3025,8 @@ export const $ZodNullable: core.$constructor<$ZodNullable> = /*@__PURE__*/ core.
   "$ZodNullable",
   (inst, def) => {
     $ZodType.init(inst, def);
-    util.defineLazy(inst._zod, "optionality", () => def.innerType._zod.optionality);
+    util.defineLazy(inst._zod, "optin", () => def.innerType._zod.optin);
+    util.defineLazy(inst._zod, "optout", () => def.innerType._zod.optout);
 
     util.defineLazy(inst._zod, "pattern", () => {
       const pattern = def.innerType._zod.pattern;
@@ -3052,8 +3062,7 @@ export interface $ZodDefaultDef<T extends $ZodType = $ZodType> extends $ZodTypeD
 export interface $ZodDefaultInternals<T extends $ZodType = $ZodType>
   extends $ZodTypeInternals<util.NoUndefined<core.output<T>>, core.input<T> | undefined> {
   def: $ZodDefaultDef<T>;
-  // qin: "true";
-  optionality: "defaulted";
+  optin: "optional";
   isst: never;
   values: T["_zod"]["values"];
 }
@@ -3068,7 +3077,7 @@ export const $ZodDefault: core.$constructor<$ZodDefault> = /*@__PURE__*/ core.$c
     $ZodType.init(inst, def);
 
     // inst._zod.qin = "true";
-    inst._zod.optionality = "defaulted";
+    inst._zod.optin = "optional";
     util.defineLazy(inst._zod, "values", () => def.innerType._zod.values);
 
     inst._zod.parse = (payload, ctx) => {
@@ -3113,7 +3122,7 @@ export interface $ZodPrefaultDef<T extends $ZodType = $ZodType> extends $ZodType
 export interface $ZodPrefaultInternals<T extends $ZodType = $ZodType>
   extends $ZodTypeInternals<util.NoUndefined<core.output<T>>, core.input<T> | undefined> {
   def: $ZodPrefaultDef<T>;
-  optionality: "defaulted";
+  optin: "optional";
   isst: never;
   values: T["_zod"]["values"];
 }
@@ -3127,7 +3136,7 @@ export const $ZodPrefault: core.$constructor<$ZodPrefault> = /*@__PURE__*/ core.
   (inst, def) => {
     $ZodType.init(inst, def);
 
-    inst._zod.optionality = "defaulted";
+    inst._zod.optin = "optional";
     util.defineLazy(inst._zod, "values", () => def.innerType._zod.values);
 
     inst._zod.parse = (payload, ctx) => {
@@ -3299,7 +3308,8 @@ export interface $ZodCatchInternals<T extends $ZodType = $ZodType>
   // qin: T["_zod"]["qin"];
   // qout: T["_zod"]["qout"];
 
-  optionality: T["_zod"]["optionality"];
+  optin: T["_zod"]["optin"];
+  optout: T["_zod"]["optout"];
   isst: never;
   values: T["_zod"]["values"];
 }
@@ -3310,7 +3320,8 @@ export interface $ZodCatch<T extends $ZodType = $ZodType> extends $ZodType {
 
 export const $ZodCatch: core.$constructor<$ZodCatch> = /*@__PURE__*/ core.$constructor("$ZodCatch", (inst, def) => {
   $ZodType.init(inst, def);
-  util.defineLazy(inst._zod, "optionality", () => def.innerType._zod.optionality);
+  util.defineLazy(inst._zod, "optin", () => def.innerType._zod.optin);
+  util.defineLazy(inst._zod, "optout", () => def.innerType._zod.optout);
   util.defineLazy(inst._zod, "values", () => def.innerType._zod.values);
 
   inst._zod.parse = (payload, ctx) => {
@@ -3404,6 +3415,8 @@ export interface $ZodPipeInternals<A extends $ZodType = $ZodType, B extends $Zod
   def: $ZodPipeDef<A, B>;
   isst: never;
   values: A["_zod"]["values"];
+  optin: A["_zod"]["optin"];
+  optout: B["_zod"]["optout"];
 }
 
 export interface $ZodPipe<A extends $ZodType = $ZodType, B extends $ZodType = $ZodType> extends $ZodType {
@@ -3412,9 +3425,9 @@ export interface $ZodPipe<A extends $ZodType = $ZodType, B extends $ZodType = $Z
 
 export const $ZodPipe: core.$constructor<$ZodPipe> = /*@__PURE__*/ core.$constructor("$ZodPipe", (inst, def) => {
   $ZodType.init(inst, def);
-  // inst._zod.qin = def.in._zod.qin;
-  // inst._zod.qout = def.in._zod.qout;
-  inst._zod.values = def.in._zod.values;
+  util.defineLazy(inst._zod, "values", () => def.in._zod.values);
+  util.defineLazy(inst._zod, "optin", () => def.in._zod.optin);
+  util.defineLazy(inst._zod, "optout", () => def.out._zod.optout);
 
   inst._zod.parse = (payload, ctx) => {
     const left = def.in._zod.run(payload, ctx);
@@ -3449,9 +3462,8 @@ export interface $ZodReadonlyDef extends $ZodTypeDef {
 export interface $ZodReadonlyInternals<T extends $ZodType = $ZodType>
   extends $ZodTypeInternals<util.MakeReadonly<core.output<T>>, util.MakeReadonly<core.input<T>>> {
   def: $ZodReadonlyDef;
-  // qin: T["_zod"]["qin"];
-  // qout: T["_zod"]["qout"];
-  optionality: T["_zod"]["optionality"];
+  optin: T["_zod"]["optin"];
+  optout: T["_zod"]["optout"];
   isst: never;
   disc: T["_zod"]["disc"];
 }
@@ -3465,7 +3477,8 @@ export const $ZodReadonly: core.$constructor<$ZodReadonly> = /*@__PURE__*/ core.
   (inst, def) => {
     $ZodType.init(inst, def);
     util.defineLazy(inst._zod, "disc", () => def.innerType._zod.disc);
-    util.defineLazy(inst._zod, "optionality", () => def.innerType._zod.optionality);
+    util.defineLazy(inst._zod, "optin", () => def.innerType._zod.optin);
+    util.defineLazy(inst._zod, "optout", () => def.innerType._zod.optout);
 
     inst._zod.parse = (payload, ctx) => {
       const result = def.innerType._zod.run(payload, ctx);
@@ -3638,9 +3651,8 @@ export interface $ZodLazyInternals<T extends $ZodType = $ZodType>
   innerType: T;
   pattern: T["_zod"]["pattern"];
   disc: T["_zod"]["disc"];
-  // qin: T["_zod"]["qin"];
-  // qout: T["_zod"]["qout"];
-  optionality: T["_zod"]["optionality"];
+  optin: T["_zod"]["optin"];
+  optout: T["_zod"]["optout"];
 }
 
 export interface $ZodLazy<T extends $ZodType = $ZodType> extends $ZodType {
@@ -3653,7 +3665,8 @@ export const $ZodLazy: core.$constructor<$ZodLazy> = /*@__PURE__*/ core.$constru
   util.defineLazy(inst._zod, "innerType", () => def.getter());
   util.defineLazy(inst._zod, "pattern", () => inst._zod.innerType._zod.pattern);
   util.defineLazy(inst._zod, "disc", () => inst._zod.innerType._zod.disc);
-  util.defineLazy(inst._zod, "optionality", () => inst._zod.innerType._zod.optionality);
+  util.defineLazy(inst._zod, "optin", () => inst._zod.innerType._zod.optin);
+  util.defineLazy(inst._zod, "optout", () => inst._zod.innerType._zod.optout);
   inst._zod.parse = (payload, ctx) => {
     const inner = inst._zod.innerType;
     return inner._zod.run(payload, ctx);
diff --git a/packages/zod/src/v4/core/to-json-schema.ts b/packages/zod/src/v4/core/to-json-schema.ts
index 8d9effaf..ebc75608 100644
--- a/packages/zod/src/v4/core/to-json-schema.ts
+++ b/packages/zod/src/v4/core/to-json-schema.ts
@@ -273,11 +273,11 @@ export class JSONSchemaGenerator {
           // const optionalKeys = new Set(def.optional);
           const requiredKeys = new Set(
             [...allKeys].filter((key) => {
-              const opt = def.shape[key]._zod.optionality;
+              const v = def.shape[key]._zod;
               if (this.io === "input") {
-                return opt === undefined;
+                return v.optin === undefined;
               } else {
-                return opt === undefined || opt === "defaulted";
+                return v.optout === undefined;
               }
             })
           );
diff --git a/packages/zod/src/v4/core/util.ts b/packages/zod/src/v4/core/util.ts
index d235fdb1..0537bfc4 100644
--- a/packages/zod/src/v4/core/util.ts
+++ b/packages/zod/src/v4/core/util.ts
@@ -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.optionality === "optional";
+    return shape[k]._zod.optin === "optional";
   });
 }
 
diff --git a/play.ts b/play.ts
index 22dbc4e9..f028c087 100644
--- a/play.ts
+++ b/play.ts
@@ -1,24 +1,10 @@
 import * as z from "zod/v4";
 
-// console.dir(z.literal("asdf").optional()._zod.values, { depth: null });
-
-const BaseError = z.object({ status: z.literal("failed"), message: z.string() });
-const MyErrors = z.discriminatedUnion("code", [
-  BaseError.extend({ code: z.literal(400) }),
-  BaseError.extend({ code: z.literal(401) }),
-  BaseError.extend({ code: z.literal(500) }),
-]);
-
-const MyResult = z.discriminatedUnion("status", [
-  z.object({ status: z.literal("success"), data: z.string() }),
-  MyErrors,
-]);
-
-const result = MyResult.parse({ status: "success", data: "hello" });
-console.log(result); // { status: 'success', data: 'hello' }
-const result2 = MyResult.parse({ status: "failed", code: 400, message: "bad request" });
-console.log(result2); // { status: 'failed', code: 400, message: 'bad request' }
-const result3 = MyResult.parse({ status: "failed", code: 401, message: "unauthorized" });
-console.log(result3); // { status: 'failed', code: 401, message: 'unauthorized' }
-const result4 = MyResult.parse({ status: "failed", code: 500, message: "internal server error" });
-console.log(result4); // { status: 'failed', code: 500, message: 'internal server error' }
+const data = z
+  .object({
+    foo: z.boolean().nullable().default(true),
+    bar: z.boolean().default(true),
+  })
+  .parse({ foo: null }, { jitless: false });
+
+console.dir(data, { depth: null });


Test patch:
diff --git a/packages/zod/src/v4/classic/tests/lazy.test.ts b/packages/zod/src/v4/classic/tests/lazy.test.ts
index f4db9d8c..fb023480 100644
--- a/packages/zod/src/v4/classic/tests/lazy.test.ts
+++ b/packages/zod/src/v4/classic/tests/lazy.test.ts
@@ -36,9 +36,14 @@ test("opt passthrough", () => {
     c: "default",
   });
 
-  expect(z.lazy(() => z.string())._zod.optionality).toEqual(undefined);
-  expect(z.lazy(() => z.string().optional())._zod.optionality).toEqual("optional");
-  expect(z.lazy(() => z.string().default("asdf"))._zod.optionality).toEqual("defaulted");
+  expect(z.lazy(() => z.string())._zod.optin).toEqual(undefined);
+  expect(z.lazy(() => z.string())._zod.optout).toEqual(undefined);
+
+  expect(z.lazy(() => z.string().optional())._zod.optin).toEqual("optional");
+  expect(z.lazy(() => z.string().optional())._zod.optout).toEqual("optional");
+
+  expect(z.lazy(() => z.string().default("asdf"))._zod.optin).toEqual("optional");
+  expect(z.lazy(() => z.string().default("asdf"))._zod.optout).toEqual(undefined);
 });
 
 //////////////   LAZY   //////////////
diff --git a/packages/zod/src/v4/classic/tests/optional.test.ts b/packages/zod/src/v4/classic/tests/optional.test.ts
index 0de0083e..1fc142b7 100644
--- a/packages/zod/src/v4/classic/tests/optional.test.ts
+++ b/packages/zod/src/v4/classic/tests/optional.test.ts
@@ -16,3 +16,88 @@ test("unwrap", () => {
   const unwrapped = z.string().optional().unwrap();
   expect(unwrapped).toBeInstanceOf(z.ZodString);
 });
+
+test("optionality", () => {
+  const a = z.string();
+  expect(a._zod.optin).toEqual(undefined);
+  expect(a._zod.optout).toEqual(undefined);
+
+  const b = z.string().optional();
+  expect(b._zod.optin).toEqual("optional");
+  expect(b._zod.optout).toEqual("optional");
+
+  const c = z.string().default("asdf");
+  expect(c._zod.optin).toEqual("optional");
+  expect(c._zod.optout).toEqual(undefined);
+
+  const d = z.string().optional().nullable();
+  expect(d._zod.optin).toEqual("optional");
+  expect(d._zod.optout).toEqual("optional");
+
+  const e = z.string().default("asdf").nullable();
+  expect(e._zod.optin).toEqual("optional");
+  expect(e._zod.optout).toEqual(undefined);
+});
+
+test("pipe optionality", () => {
+  z.string().optional()._zod.optin;
+  const a = z.string().optional().pipe(z.string());
+  expect(a._zod.optin).toEqual("optional");
+  expect(a._zod.optout).toEqual(undefined);
+  expectTypeOf<typeof a._zod.optin>().toEqualTypeOf<"optional">();
+  expectTypeOf<typeof a._zod.optout>().toEqualTypeOf<"optional" | undefined>();
+
+  const b = z
+    .string()
+    .transform((val) => (Math.random() ? val : undefined))
+    .pipe(z.string().optional());
+  expect(b._zod.optin).toEqual(undefined);
+  expect(b._zod.optout).toEqual("optional");
+  expectTypeOf<typeof b._zod.optin>().toEqualTypeOf<"optional" | undefined>();
+  expectTypeOf<typeof b._zod.optout>().toEqualTypeOf<"optional">();
+
+  const c = z.string().default("asdf").pipe(z.string());
+  expect(c._zod.optin).toEqual("optional");
+  expect(c._zod.optout).toEqual(undefined);
+
+  const d = z
+    .string()
+    .transform((val) => (Math.random() ? val : undefined))
+    .pipe(z.string().default("asdf"));
+  expect(d._zod.optin).toEqual(undefined);
+  expect(d._zod.optout).toEqual(undefined);
+});
+
+test("pipe optionality inside objects", () => {
+  const schema = z.object({
+    a: z.string().optional(),
+    b: z.string().optional().pipe(z.string()),
+    c: z.string().default("asdf").pipe(z.string()),
+    d: z
+      .string()
+      .transform((val) => (Math.random() ? val : undefined))
+      .pipe(z.string().optional()),
+    e: z
+      .string()
+      .transform((val) => (Math.random() ? val : undefined))
+      .pipe(z.string().default("asdf")),
+  });
+
+  type SchemaIn = z.input<typeof schema>;
+  expectTypeOf<SchemaIn>().toEqualTypeOf<{
+    a?: string | undefined;
+    b?: string | undefined;
+    c?: string | undefined;
+    d: string;
+    e: string;
+  }>();
+
+  type SchemaOut = z.output<typeof schema>;
+  expectTypeOf<SchemaOut>().toEqualTypeOf<{
+    a?: string | undefined;
+    b: string;
+    c: string;
+    d?: string | undefined;
+    e: string;
+  }>();
+});
diff --git a/packages/zod/src/v4/classic/tests/pickomit.test.ts b/packages/zod/src/v4/classic/tests/pickomit.test.ts
index 5e8819c8..39f45e84 100644
--- a/packages/zod/src/v4/classic/tests/pickomit.test.ts
+++ b/packages/zod/src/v4/classic/tests/pickomit.test.ts
@@ -44,10 +44,10 @@ test("pick parse - fail", () => {
 
 test("pick - remove optional", () => {
   const schema = z.object({ a: z.string(), b: z.string().optional() });
-  expect(schema._zod.def.shape.a._zod.optionality).toEqual(undefined);
-  expect(schema._zod.def.shape.b!._zod.optionality).toEqual("optional");
+  expect("a" in schema._zod.def.shape).toEqual(true);
+  expect("b" in schema._zod.def.shape!).toEqual(true);
   const picked = schema.pick({ a: true });
-  expect(picked._zod.def.shape.a._zod.optionality).toEqual(undefined);
+  expect("a" in picked._zod.def.shape).toEqual(true);
   expect("b" in picked._zod.def.shape!).toEqual(false);
 });
 
@@ -85,9 +85,9 @@ test("omit parse - fail", () => {
 
 test("omit - remove optional", () => {
   const schema = z.object({ a: z.string(), b: z.string().optional() });
+  expect("a" in schema._zod.def.shape).toEqual(true);
   const omitted = schema.omit({ a: true });
   expect("a" in omitted._zod.def.shape).toEqual(false);
-  expect(omitted._zod.def.shape.b!._zod.optionality).toEqual("optional");
 });
 
 test("nonstrict inference", () => {
diff --git a/packages/zod/src/v4/mini/tests/object.test.ts b/packages/zod/src/v4/mini/tests/object.test.ts
index 46fad849..24c422e6 100644
--- a/packages/zod/src/v4/mini/tests/object.test.ts
+++ b/packages/zod/src/v4/mini/tests/object.test.ts
@@ -10,7 +10,7 @@ test("z.object", () => {
   });
 
   a._zod.def.shape["test?"];
-  a._zod.def.shape.points._zod.optionality;
+  a._zod.def.shape.points._zod.optin;
 
   type a = z.output<typeof a>;