.default() should apply default if wrapped schema transforms to undefined
This may seem strange, but it's because I've been dealing with AWS CloudFormation parameters, where you can only have empty strings, not null or undefined values...
I was trying to do
// helper used in place of `.optional()` for my schema for parsing CloudFormation metadata
function optional<T extends z.ZodTypeAny>(schema: T) {
return z
.union([z.literal('').transform(() => undefined), schema.optional()])
.optional()
}
const parameter = optional(z.string()).default('test')
I expected parameter.parse('') to be 'test' but instead it's undefined. This violates the output type of .default('test'), so I think even if the input is defined, ZodDefault should check that the parse output is too.
Here we're seeing the consequence of designing ZodDefault to change the input to a default value if it's undefined, rather than just changing the output value if it's undefined.
To guarantee that ZodDefault does produce the correct output type, I had to:
- Re-parse on the default value if parsing a non-null value output undefined
- In the worst case, even parsing the default value outputs undefined -- I had to add an error for this case
Here we're seeing the consequence of designing ZodDefault to change the input to a default value if it's undefined, rather than just changing the output value if it's undefined.
Indeed, ZodDefault is essentially a "short circuit" if the input is undefined. In my experience the majority of people expect it to work this way - operating as a "pre-check" instead of a "post-check". Though few people are every in a position to notice the difference.
The noUndefined that's used on the output type is also a decision that is more pragmatic than sound. A lot of people do z.string().optional().default() and expect the output type to be just string.
Perhaps Zod should just do both...short circuit with defaultValue if the input is undefined, otherwise run innerType.parse, check if the result if undefined, and return defaultValue if so. It feels insane but it might be the approach that behaves as expected for the greatest number of people (and it makes the output type with noUndefined 100% correct.
Let me know what you think, I'm evaluating changes to this behavior for Zod 4. cc @jedwards1211
Perhaps Zod should just do both...short circuit with defaultValue if the input is undefined, otherwise run innerType.parse, check if the result if undefined, and return defaultValue if so.
That's what I did in #3615. However, if a second default is applied, it's still impossible to get it to return the latter default if the initial input was not undefined. But I think it's an improvement nonetheless
In my experience the majority of people expect it to work this way - operating as a "pre-check" instead of a "post-check"
Was there a time when it didn't short-circuit, and then people complained that earlier nodes in the chain were running? Or was it just the fact that people asked for .default(a).default(b) to apply the latter default? (There would be ways to do that without short circuiting.)
Maybe Zod should add a .coalesce that does nullish coalescing in a non-short-circuit manner.
Hi, @jedwards1211. I'm Dosu, and I'm helping the Zod team manage their backlog. I'm marking this issue as stale.
Issue Summary:
- The issue involves the
.default()method not applying a default value if the wrapped schema transforms the input toundefined. - You suggested that
ZodDefaultshould ensure the output is defined, especially for AWS CloudFormation parameters. - @colinhacks acknowledged the design choice and mentioned a potential change for Zod 4.
- You implemented a solution in pull request #3615 and proposed adding a
.coalescemethod for non-short-circuit behavior.
Next Steps:
- Please let me know if this issue is still relevant to the latest version of Zod. If so, you can keep the discussion open by commenting on the issue.
- Otherwise, the issue will be automatically closed in 14 days.
Thank you for your understanding and contribution!