question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Consider adding `lenient()` option for string inputs

See original GitHub issue

I 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:closed
  • Created 2 years ago
  • Reactions:5
  • Comments:11

github_iconTop GitHub Comments

1reaction
RichiCoder1commented, Dec 28, 2021

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.

1reaction
scotttrinhcommented, Dec 1, 2021

I have a small issue with the implicit implication that the original use case I provided for something like lenient() is “opinionated” and/or can be dismissed as being “sensitive to case-by-case variation”. I think it’s fair to say that no library will cover every use case. That’s a given. But I frankly can’t remember the last time I had to work on a node web service and did not need to parse/validate string data.

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!

The argument is more centered around developer experience.

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 without BigInt. 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 like number -> string, etc.

Since you brought up the topic of libraries, after seeing what other libraries closer to zod exist in the ecosystem, I think it’s safe to say that a very important function of zod is to provide safe typings and parsing to web APIs (tRPC, json-schema-to-zod, etc.). With that in mind, I can’t help but wonder: what is it not straightforward to express the type for something like GET /users/:id, with id being a number, with a zod schema?

From my perspective, the schema for that (z.string().transform(Number)) is perfectly straightforward. And if you’re writing a library like tRPC maybe you have some more robust schemas that check for NaN and undefined 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” as string 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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Mockito.lenient does not return expected result - Stack Overflow
"doWork" method seems like taking two String and one Date object. In the test method you should give proper inputs to "doWork" to...
Read more >
Mitigate cross-site scripting (XSS) with a strict Content Security ...
Learn how to deploy a CSP based on script nonces or hashes as a defense-in-depth against cross-site scripting.
Read more >
12.8 String Functions and Operators - MySQL :: Developer Zone
Reads the file and returns the file contents as a string. To use this function, the file must be located on the server...
Read more >
TSConfig Reference - Docs on every TSConfig option
By default, TypeScript will examine the initial set of files for import and <reference directives and add these resolved files to your program....
Read more >
Input Validation - OWASP Cheat Sheet Series
parseInt() in Java, int() in Python) with strict exception handling ... expressions for any other structured data covering the whole input string (^....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found