feat(v4): clearer error for missing required object properties#5938
feat(v4): clearer error for missing required object properties#5938dokson wants to merge 2 commits into
Conversation
…hacks#5917) When a required key is absent from an object, the `invalid_type` issue now carries `received: "missing"` and the English locale renders it as `Invalid input: missing required property`. Previously every missing-key case shared the same message as an explicit `z.nonoptional()` rejecting `undefined` ("expected nonoptional, received undefined"), even though the user never sent any value. Since the v4.4.0 change that makes `z.undefined()` properties required, this ambiguity has surfaced repeatedly (e.g. vercel/workflow#1901): users read the error as a Zod bug rather than as "this key must be present". Behavior: - Missing key (`{ value: z.undefined() }` parsed against `{}`): `Invalid input: missing required property` + `received: "missing"`. - Explicit `z.nonoptional(...)` receiving `undefined`: unchanged ("expected nonoptional, received undefined"), since the value really was received. The new field is additive on `$ZodIssueInvalidType`. Code stays `"invalid_type"` and `expected` stays `"nonoptional"`, so consumers matching by code/expected keep working. Other locales fall back to the existing message until updated.
|
TL;DR — When a required object key is absent from the input, Zod now emits Key changes
Summary | 6 files | 2 commits | base: Distinct error for absent keys vs. explicit
|
There was a problem hiding this comment.
Reviewed — no issues found.
Claude Opus | 𝕏
…g-property-error # Conflicts: # packages/zod/src/v4/classic/tests/partial.test.ts
|
@colinhacks just rebased on top of main to resolve the conflicts from #5937/#5941. I think this PR still has merit even after those landed: #5941 made Happy to adjust if you'd prefer a different framing. |
Refs #5917 (and the follow-up comment). Pure DX — does not change validation behavior.
Problem
When a required key is absent from an object input, Zod emits the same issue as an explicit
z.nonoptional(...)rejectingundefined:Since v4.4.0 made
z.undefined()properties required, this message has been recurring confusion (e.g. vercel/workflow#1901). Users read it as a Zod bug — "I never sentundefined, the key isn't there" — and don't immediately see that the fix is.optional()on their schema.The two cases have different root causes but share one message:
z.objectcore/schemas.ts:1764,:2050z.nonoptional(...)receivingundefinedcore/schemas.ts:3760Fix
Distinguish the two by tagging the missing-key issue with
received: "missing", and render that case asInvalid input: missing required propertyin the English locale. The explicit-undefinedcase keeps its current message — there, "received undefined" is accurate.// core/schemas.ts — both missing-key emit sites final.issues.push({ code: "invalid_type", expected: "nonoptional", input: undefined, path: [key], + received: "missing", });// locales/en.ts case "invalid_type": { + if (issue.received === "missing") { + return `Invalid input: missing required property`; + } const expected = TypeDictionary[issue.expected] ?? issue.expected; const receivedType = util.parsedType(issue.input); const received = TypeDictionary[receivedType] ?? receivedType; return `Invalid input: expected ${expected}, received ${received}`; }$ZodIssueInvalidType.receivedalready existed informally (used by primitive parsers to mark"NaN","Infinity","Invalid Date"); this PR formalizes it on the type and adds"missing".Effect on the Vercel Workflow repro
Before:
After:
The actual fix for that case still belongs to vercel/workflow (those fields need
.optional()per the v4.4.0 release notes), but at least the error now points the user there instead of looking like a Zod bug.Backward compatibility
codestays"invalid_type",expectedstays"nonoptional"— consumers matching by code/expected keep working.receivedis optional and additive on$ZodIssueInvalidType.Tests
packages/zod/src/v4/classic/tests/missing-required-property.test.ts:received: "missing".z.string().optional().nonoptional()rejectingundefined→ unchanged message, noreceivedfield (non-regression).Updated existing snapshots that asserted the old string for missing-key cases:
optional.test.ts("object absent keys require optin optional")partial.test.ts("catch/prefault/default")nonoptional.test.tssnapshots are deliberately untouched: they go throughhandleNonOptionalResult(explicit-undefinedpath), not the missing-key path.Local results:
(One flaky
redos checkertest fails locally on Windows, same flake mentioned in #5923 / fixed upstream by #5919 — unrelated.)Note about CI
Test with TypeScript latestwill likely show red for the samebaseUrldeprecation that affects every PR opened sincee58ea4d; addressed by #5921.