Fix z.any missing object keys#6000
Conversation
There was a problem hiding this comment.
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
$ZodAnyas optional on input — adds the literaloptin: "optional"on$ZodAnyInternalsand assignsinst._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/optoutin theoptionalitytest, and a JIT + jitlessz.object({ value: z.any() }).parse({})case inobject absent keys require optin optional.
Summary | 2 files | 1 commit | base: main ← fix-any-object-missing-key-5997
Fix scope and a parallel case worth flagging
Before:
z.object({ a: z.any() }).parse({})threwinvalid_type/expected nonoptionalon 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
Claude Opus | 𝕏
|
|
||
| export const $ZodAny: core.$constructor<$ZodAny> = /*@__PURE__*/ core.$constructor("$ZodAny", (inst, def) => { | ||
| $ZodType.init(inst, def); | ||
| inst._zod.optin = "optional"; |
There was a problem hiding this comment.
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().

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