zod
zod copied to clipboard
feat: add `decode` and `safeDecode` methods to check type at build time instead of runtime
This adds the following methods:
-
decode
-
safeDecode
-
decodeAsync
-
safeDecodeAsync
Those methods are extremely similar to their parse
counterparts, the differences are:
- They expect a
Input
as an argument instead ofunknown
.
Using zod as such is therefore possible:
const schema = z.object({ k: z.number().min(1) });
const valid = { k: 12 };
const invalidWrongType = "value";
const invalidValue = { k: 0 };
const invalidUnknown = { k: 12 } as unknown;
schema.decode(valid) // success
schema.decode(invalidWrongType) // Typescript error
schema.decode(invalidValue) // Runtime error
schema.decode(invalidUnknown) // Typescript error, should use .parse instead
Checks, refiners, transformers are still run, it just delegates type checking to typescript?
The main use case is when you want to apply business rules to multiple fields that can return an error, but primitives types have already been checked some other way, for instance:
- Schema-first gRPC/graphql servers with generated stub files that check messages/queries already.
- Databases accessed using some kind of ORM that already checks primitives.
For instance, I have a gRPC server and I want to transform the message { email: string; phoneNumber: string | null }
into { email: EmailAddress; phoneNumber: PhoneNumber | null }
Without something like zod, it can be painful:
try {
const email = EmailAddress.parse(request.email);
const phoneNumber = request.phoneNumber ? PhoneNumber.parse(request.phoneNumber) : null;
// rest of your code
} catch (e) {
// handle error
}
It's even more painful when your parse
methods are safe and do not throw.
const email = EmailAddress.parse(request.email);
const phoneNumber = request.phoneNumber ? PhoneNumber.parse(request.phoneNumber) : null;
if (email instanceof Error || phoneNumber instanceof Error) {
// handle error
}
With zod and the decode
methods, I can ensure that my zod schema matches my gRPC message, which results in cleaner and more reusable/composable pieces of code:
const schema = z.object({
email: z.string().refine(/* ... */).transform(/* ... */),
phoneNumer: z.string().refine(/* ... */).transform(/* ... */).nullable(), // see the typo here in the key?
})
const parsed = schema.safeDecode(request) // Typescript error, key phoneNumer is expected but received phoneNumber
if (!parsed.success) {
// handle error
}
Some more info in this discussion https://github.com/colinhacks/zod/discussions/1335
I'm not too sure about:
- Should we keep validating the type at runtime to prevent unexpected errors when the type of the input is
any
? - Are these changes enough, it seems like some structural changes could be good to reduce the number of paths.
Deploy Preview for guileless-rolypoly-866f8a ready!
Built without sensitive environment variables
Name | Link |
---|---|
Latest commit | 23b297359a8c30507bca6d34b3074c00ef52ddc3 |
Latest deploy log | https://app.netlify.com/sites/guileless-rolypoly-866f8a/deploys/63171c1dc6d89f0008fe2850 |
Deploy Preview | https://deploy-preview-1342--guileless-rolypoly-866f8a.netlify.app |
Preview on mobile | Toggle QR Code...Use your smartphone camera to open QR code link. |
To edit notification comments on pull requests, go to your Netlify site settings.
Sorry, I'm not really seeing the appeal of this. I get that it's somewhat duplicative, but I recommend using regular .parse
here? It's weird to provide a method that skips some checks (type validations) but not others (refinements). This distinction is hard to explain to users who aren't familiar with Zod internals.
I suppose I'm not clear either on whether your concern here is performance or DX/API. It seems like you want to skip validations but apply transforms? You may be better suited just defining your transform logic externally to Zod in this case. I don't think this use case is popular enough to merit a major addition to the API.
@colinhacks My only usecase is that parse
expects an unknown
object and sometimes it doesn't make sense. Sometimes you have a schema like this one: const dateTimeSchema = z.string().refine(externalDateTimeValidationFunction)
and you want to check that something you already know is a string
is valid. But dateTimeSchema.parse(3)
throws a runtime error, when it could have been caught by typescript at build time if the decode
method, added in this PR, existed.
Example of where it can be useful: You have two APIs that implement the same usecase and need the same data. You write a zod schema for them both:
z.object({
id: z.string().refine(isUUID),
prop1: z.string().nullable(),
})
- a REST API with a
body: unknown
that youparse
using a zod schema. - a gRPC API that gives you a
message: { id: string, prop1: string | null }
that youdecode
using the same zod schema.Advantage
: If the type ofmessage
is not compatible with the zod schema input, there will be a typescript error, no risk this will be sent in production.
The removal of runtime validations checks can be reverted, I just did it because it felt like they weren't necessary with decode
.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.