Consider adding `lenient()` option for string inputs
See original GitHub issueI was in the process of migrating my existing yup
schemas to zod
and found that the main sticking point is handling the parsing/validation of request path and query string parameters. Since they are typically considered raw strings and the parsing is left to the application level, zod
doesn’t really provide a great DX in these scenarios. Specially when compared to how yup
does it out-of-the-box.
// My existing `yup` schema
const schema = yup.object({ params: yup.object({ id: yup.number().required() }) })
// GET /user/42
// req -> { params: { id: '42' } }
schema.validateSync(req) // { params: { id: 42 } } -> Passes
// `zod` equivalent
const schema = z.object({ params: z.object({ id: z.number() }) })
// GET /user/42
// req -> { params: { id: '42' } }
schema.parse(req) // -> Fails
/*
Uncaught:
[
{
"code": "invalid_type",
"expected": "number",
"received": "string",
"path": [
"params",
"id"
],
"message": "Expected number, received string"
}
]
*/
Which leads me to write custom preprocess()
validators, along with custom error messages, for each expected type, every time. Here’s an example for validating numerical strings.
const DEFAULT_ZOD_NUMERICAL_PARAMS = Object.freeze({
errorMap: (issue, _ctx) => {
const message =
issue.code === z.ZodIssueCode.invalid_type
? `Expected ${issue.expected}, received ${issue.received === 'string' ? `'${_ctx.data}'` : issue.received}`
: _ctx.defaultError
return { message }
},
})
function numerical(params) {
return z.preprocess(value => {
const num = isNilOrEmpty(value) ? NaN : Number(value)
return Number.isNaN(num) ? value : num
}, z.number(params ?? DEFAULT_ZOD_NUMERICAL_PARAMS))
}
export default numerical
I know this has been discussed before, but having something close to a .lenient()
parsing option, allowing for values to be internally coerced would be great.
z.lenient(z.number()).parse('42')
// 42
IMHO, this is such a common scenario when dealing with serialized data, that it only makes sense for a library such as this to support it without extra hassle. In addition, while the .preprocess()
method above works, it transfers the responsibility of the parsing to the user, which is arguably the main use case of zod
.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:5
- Comments:11
Top GitHub Comments
I ran into this for a rather weird use case where we’re currently using a system that “stringifys” all the properties passed in. So we get a correctly shaped object, but all the booleans/numbers/etc… are stringified. It’s another edge case I’ll admit, but it is a case.
Of the above the most “significant” issue personally is the inability to chain. So even if I wanted to write my own “type” I end up having to go through contortions to make it look like a normal type.
Right, but as your example pointed out, you think the parser should accept numbers also, and if the string is empty it should throw an error. I think that’s perfect valid, but that’s not at all how I would want something similar to act. I think that’s what I’m trying to say when I say that I think each developer (or team) needs to make decisions about how, when, and in what way serialized data in converted, and that providing functionality that picks a way is necessarily opinionated. I don’t mean to be dismissive: I think your approach is a good one that makes sense for some use cases!
I 100% agree, and a lot of people have brought up other such use cases: especially forms. In each case, you might want to make different decisions about how to cast. For another example,
pg
converts some “serialized” data already for you, but leaves some types as strings since they can be round-trip lossy withoutBigInt
. Making it easy to write the layer on-top of Zod that is appropriate for each team and use case is absolutely a part of what I see as Zod’s responsibility (preprocess
,transform
,refine
, etc). Providing implementations for each use case is something I wouldn’t want to see Zod take on, and as the link you posted in your first comment attests to, I don’t believe @colinhacks wants there to be multiple ways to transform data for certain cases likenumber -> string
, etc.From my perspective, the schema for that (
z.string().transform(Number)
) is perfectly straightforward. And if you’re writing a library liketRPC
maybe you have some more robust schemas that check forNaN
andundefined
and return some appropriate error, but I think that kind of logic belongs in that library rather than in Zod. I think it makes sense that Zod treats types in a similar way to TypeScript (TypeScript treats that “type” asstring
also) while giving the affordances for transformation such that the input type and the output type might be different.I don’t mean to come across as confrontational, and I very much appreciate your perspective and thoughtful answers and suggestions here. My hope is that users with the right vantage point based on their expertise and opinions can provide the layer that you feel we’re missing, and I very much agree with you that the ecosystem is missing these sorts of developer-friendly and use-case specific libraries. I am also frustrated that I have to write these transforms by hand, but even if we provided your specific solution, I would still write them by hand since they do not align with my team’s specific viewpoint on the proper way to specify these schemas for the multitude of use cases we have (json, query strings, form data, database data, etc.). I hope my comments help to situate my opinion (and that’s all this is: my opinion!) about the direction I’d like to see Zod take and don’t dissuade you from continuing to advocate for your own perspective.