Skip to content

Fix z.any missing object keys#6000

Open
cyphercodes wants to merge 1 commit into
colinhacks:mainfrom
cyphercodes:fix-any-object-missing-key-5997
Open

Fix z.any missing object keys#6000
cyphercodes wants to merge 1 commit into
colinhacks:mainfrom
cyphercodes:fix-any-object-missing-key-5997

Conversation

@cyphercodes
Copy link
Copy Markdown
Contributor

Fixes #5997.

z.any() accepts undefined, so absent object keys using z.any() should not be rejected by the missing-key guard before the schema result is handled. This marks z.any() as optional on input while preserving its output requiredness, with coverage for both JIT and jitless object parsing.

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR — Restores 4.3.x behavior for z.object({ k: z.any() }).parse({}) by marking z.any() as optin: "optional", so the object missing-key guard at handlePropertyResult no longer rejects absent z.any() keys. Output requiredness is preserved (optout stays undefined), so existing z.infer<> shapes are unchanged.

Key changes

  • Mark $ZodAny as optional on input — adds the literal optin: "optional" on $ZodAnyInternals and assigns inst._zod.optin = "optional" in the constructor, matching the pattern used by $ZodDefault / $ZodPrefault / $ZodCatch.
  • Coverage for the regression — new assertions on z.any()._zod.optin/optout in the optionality test, and a JIT + jitless z.object({ value: z.any() }).parse({}) case in object absent keys require optin optional.

Summary | 2 files | 1 commit | base: mainfix-any-object-missing-key-5997


Fix scope and a parallel case worth flagging

Before: z.object({ a: z.any() }).parse({}) threw invalid_type / expected nonoptional on 4.4.x.
After: Parses to {}, matching 4.3.x semantics; { a: undefined } still produces { a: undefined }.

The fix is minimal and targeted at the exact seam called out in the learnings note on optin/optout semantics: z.object consults el._zod.optin === "optional" before the entry's schema runs, and $ZodAny previously didn't set that flag. Setting only optin (not optout) is the right call — output type for anyRequired: z.any() stays any (not any | undefined-keyed-optional), keeping the existing inferred type for unknown/any keys test in object.test.ts green.

One scoping question: z.unknown() has the same shape — $ZodUnknownInternals also lacks optin: "optional", and z.object({ a: z.unknown() }).parse({}) exhibits the same regression class as #5997. This PR scopes to z.any() because that's what the issue reports, which is reasonable, but @colinhacks may want to fold z.unknown() in here or track it separately.

packages/zod/src/v4/core/schemas.ts · packages/zod/src/v4/classic/tests/optional.test.ts

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏


export const $ZodAny: core.$constructor<$ZodAny> = /*@__PURE__*/ core.$constructor("$ZodAny", (inst, def) => {
$ZodType.init(inst, def);
inst._zod.optin = "optional";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider applying the same change to $ZodUnknown (lines 1465-1481). z.unknown() accepts undefined and exhibits the same regression — z.object({ a: z.unknown() }).parse({}) throws expected nonoptional on 4.4.x for the same reason. Optional follow-up; the linked issue scopes only to z.any().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Undocumented (?) change to z.any() object property behavior in v4.4.0

1 participant