From f32ddf9e581d5ba5f0278ad26b1bfb9ff8f8a6dd Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Wed, 29 Apr 2026 10:25:14 -0700 Subject: [PATCH 1/2] fix: drop `z.undefined()`'s `optout = "optional"` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundled into #4769 alongside the legitimate `optin = "optional"` fix for #4768 without separate justification. The test added in the same commit literally has `// z.undefined should NOT be optional` directly above an assertion saying it is. `optout` is read by the object parser's absent-key error-swallow path (#5589) and the tuple parser's `optStart`. Marking `z.undefined()` optout-optional conflates "value type is undefined" with "key may be absent in inferred output," which is upstream of the confusion in #5654 and #5661. Inference doesn't move: `\$ZodUndefinedInternals` doesn't promote `optin`/`optout` to required fields, so `z.object({ a: z.undefined() })` already infers as `{ a: undefined }` regardless. `optin = "optional"` stays — that one is the JSON-schema `required`-array fix from #4768. --- packages/zod/src/v4/classic/tests/optional.test.ts | 4 ++-- packages/zod/src/v4/core/schemas.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/zod/src/v4/classic/tests/optional.test.ts b/packages/zod/src/v4/classic/tests/optional.test.ts index 0bd024bfe5..b242f97360 100644 --- a/packages/zod/src/v4/classic/tests/optional.test.ts +++ b/packages/zod/src/v4/classic/tests/optional.test.ts @@ -41,14 +41,14 @@ test("optionality", () => { // z.undefined should NOT be optional const f = z.undefined(); expect(f._zod.optin).toEqual("optional"); - expect(f._zod.optout).toEqual("optional"); + expect(f._zod.optout).toEqual(undefined); expectTypeOf().toEqualTypeOf<"optional" | undefined>(); expectTypeOf().toEqualTypeOf<"optional" | undefined>(); // z.union should be optional if any of the types are optional const g = z.union([z.string(), z.undefined()]); expect(g._zod.optin).toEqual("optional"); - expect(g._zod.optout).toEqual("optional"); + expect(g._zod.optout).toEqual(undefined); expectTypeOf().toEqualTypeOf<"optional" | undefined>(); expectTypeOf().toEqualTypeOf<"optional" | undefined>(); diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index c3677f857f..212c637908 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -1359,7 +1359,6 @@ export const $ZodUndefined: core.$constructor<$ZodUndefined> = /*@__PURE__*/ cor inst._zod.pattern = regexes.undefined; inst._zod.values = new Set([undefined]); inst._zod.optin = "optional"; - inst._zod.optout = "optional"; inst._zod.parse = (payload, _ctx) => { const input = payload.value; From 9676702b7c96939f455de49b9bb479c1691774b7 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Wed, 29 Apr 2026 10:26:40 -0700 Subject: [PATCH 2/2] fix: drop `z.undefined()`'s `optin = "optional"` too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same conflation as the previous commit, on the input side. The motivating JSON-schema bug from #4768 is `.catch()`-specific; the analogous extension to `z.undefined()` was opportunistic. Under strict semantics `z.object({ a: z.undefined() })` infers as `{ a: undefined }` (required key) and the JSON-schema `required` array should agree. Runtime stays permissive — `z.undefined().parse(undefined)` succeeds whether the source was an absent key or an explicit `undefined`. --- packages/zod/src/v4/classic/tests/optional.test.ts | 4 ++-- packages/zod/src/v4/core/schemas.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/zod/src/v4/classic/tests/optional.test.ts b/packages/zod/src/v4/classic/tests/optional.test.ts index b242f97360..e8a260d91f 100644 --- a/packages/zod/src/v4/classic/tests/optional.test.ts +++ b/packages/zod/src/v4/classic/tests/optional.test.ts @@ -40,14 +40,14 @@ test("optionality", () => { // z.undefined should NOT be optional const f = z.undefined(); - expect(f._zod.optin).toEqual("optional"); + expect(f._zod.optin).toEqual(undefined); expect(f._zod.optout).toEqual(undefined); expectTypeOf().toEqualTypeOf<"optional" | undefined>(); expectTypeOf().toEqualTypeOf<"optional" | undefined>(); // z.union should be optional if any of the types are optional const g = z.union([z.string(), z.undefined()]); - expect(g._zod.optin).toEqual("optional"); + expect(g._zod.optin).toEqual(undefined); expect(g._zod.optout).toEqual(undefined); expectTypeOf().toEqualTypeOf<"optional" | undefined>(); expectTypeOf().toEqualTypeOf<"optional" | undefined>(); diff --git a/packages/zod/src/v4/core/schemas.ts b/packages/zod/src/v4/core/schemas.ts index 212c637908..baad327fd6 100644 --- a/packages/zod/src/v4/core/schemas.ts +++ b/packages/zod/src/v4/core/schemas.ts @@ -1358,7 +1358,6 @@ export const $ZodUndefined: core.$constructor<$ZodUndefined> = /*@__PURE__*/ cor $ZodType.init(inst, def); inst._zod.pattern = regexes.undefined; inst._zod.values = new Set([undefined]); - inst._zod.optin = "optional"; inst._zod.parse = (payload, _ctx) => { const input = payload.value;