Declare rejection messages on the schemas and deprecate requiredKey - #14
Merged
Merged
Conversation
The requiredKey wrapper existed to work around a type-inference collapse caused by strictNullChecks being off: zod carries a schema's optionality marker as a union with undefined, and with that flag off the union collapsed, so a required key whose schema was a union or a nullable was inferred as optional. The flag is now on and the collapse is gone -- verified by inferring an empty object against a schema with a required union key and observing it is now correctly rejected, where it previously type-checked clean. Simply deleting the wrapper would have been a regression, which is the part worth recording. It carried the rejection message, and without it zod reports a failed union as `Invalid input` -- it cannot know which member the caller intended -- and reports a failed string inside a nullable as `expected string`, which is true of the inner schema but false of the field. Both send a caller looking for the wrong fix. Zod accepts the message on the schema itself, which keeps the wording, keeps the key inferred as required, and drops the z.custom indirection. Both call sites now use that form. Three tests are added asserting the exact messages, because message quality is the thing a future cleanup would silently discard. Each was mutation-tested by removing the error parameter it guards: two turn red for the union and one for the nullable body. requiredKey is retained and marked deprecated rather than removed. It is a published export, so dropping it would break any consumer that imported it, and the inventory test asserting the public surface still lists it. The emitted parse, safeParse and Interface signatures are unchanged. The Schema objects' own declared types change from ZodCustom to ZodUnion and ZodNullable, which is what those schemas now are; the inferred output is identical, confirmed by compiling a consumer against the built package under strict mode, including that a null response body is still accepted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
requiredKeyexisted solely to work around a type-inference collapse caused bystrictNullChecksbeing off. That flag is now on, the collapse is gone, and both call sites are migrated.memberOfwas not built — checkingmainfirst showed#11already mergedmatchMember/requireMember, so building it would have been duplication. This is the other half of that follow-up.The wrapper is genuinely obsolete — measured
Probed by asserting an empty object against a schema with a required union key:
{}accepted?strictNullChecks: false)TS2739TS2739Negative control: a genuinely optional key produces 0 errors, so the probe isn't always-failing.
(My first attempt at this probe used
as never, which suppresses the exact check being tested and returned a meaningless 0. Redone.)But deleting it would have been a regression — this is the point of the PR
The wrapper also carried the rejection message. Removing it and stopping there:
valueExpected a string, number, object or array block valueInvalid inputvalueInvalid inputbodyExpected a response body string or nullexpected string, received numberZod reports a failed union as
Invalid inputbecause it cannot know which member the caller intended. Andexpected stringis true of the inner schema but false of the field, which acceptsnull— it sends a caller looking for the wrong fix.That's the "cleanup" trap: the obvious follow-up (delete the now-obsolete helper) passes every gate while quietly degrading what a caller sees.
The fix keeps both properties
Zod accepts the message on the schema itself:
Identical messages, key still inferred required, and the
z.customindirection is gone.Message quality is now tested
Three tests assert the exact strings, because this is precisely what a future cleanup discards silently. Each was mutation-tested by removing the
errorparameter it guards:error→ 2 red (should name the accepted shapes …)error→ 1 red (should name null as accepted …)requiredKeyis deprecated, not removedIt is a published export, and the inventory test asserting the public surface still lists it. Removing it would be a breaking change for any consumer that imported it. It is marked
@deprecatednaming the replacement, with the reasoning inline so the next reader doesn't re-derive it.Consumer impact: none
parse,safeParseandInterfacesignatures are unchanged across all three affected files.The
Schemaobjects' own declared types change fromZodCustom<…>toZodUnion<…>/ZodNullable<…>— which is what those schemas now are. The inferred output is identical, confirmed by compiling a real consumer against the built package understrict:The control both proves the probe works and shows the inferred union is unchanged.
(
EventData.d.tsalso shifts, since it embeds the block schema. Same character.)Verification
Every
.d.tsand.jsinlib/was diffed againstmainwith comments stripped, and the comparison was itself controlled by injecting a declaration and confirming detection. No attribution trailers; single identity for author and committer.