colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
33.33k stars 1.15k forks source link

Anyone have a good example for the docs for `z.custom` #2141

Closed JacobWeisenburger closed 1 year ago

JacobWeisenburger commented 1 year ago

the current example is not very good, because it could easily be solved with z.string().regex(). https://github.com/colinhacks/zod#custom-schemas

I would like an example where you can't solve it easily with another zod schema.

colinhacks commented 1 year ago

The difference is that the inferred type with regex would be string, whereas with custom it is a template literal

JacobWeisenburger commented 1 year ago

good point

JacobWeisenburger commented 1 year ago

I was still hoping for some other examples. Perhaps other people have some good ideas.

Mojtaba-NA commented 1 year ago

I use it for file validation.

Example for NestJS:

const fileSchema = z
  .custom<Express.Multer.File[]>()
  .refine((files) => files?.length === 1, 'single file is allowed')
  .refine((files) => files?.[0]?.size <= 5 * 1024 * 1024, "file shouldn't be more than 5 MB")
  .refine(
    (files) => ['image/jpg', 'image/jpeg', 'image/png'].includes(files?.[0]?.mimetype),
    'Accepted Formats: JPG/JPEG/PNG',
  );

I'm not sure if this can be done in some other way. (i don't provide a validation function to custom because multer takes care of that).

Example for browser (NextJS):

const fileSchema = z
  .custom<FileList>(files => files instanceof FileList)
  .refine(
    files => ['image/jpg', 'image/jpeg', 'image/png'].includes(files?.[0]?.type),
    'Accepted Formats: JPG/JPEG/PNG'
  )
  .refine(files => files?.[0]?.size <= 1 * 1024 * 1024, "Size shouldn't be more than 1 MB")
JacobWeisenburger commented 1 year ago

Thanks, I like the thought. But this seems more like a showcase of refine rather than a good use case for custom.

JacobWeisenburger commented 1 year ago
const fileSchema = z
  .custom<FileList>(files => files instanceof FileList)
  .refine(
    files => ['image/jpg', 'image/jpeg', 'image/png'].includes(files?.[0]?.type),
    'Accepted Formats: JPG/JPEG/PNG'
  )
  .refine(files => files?.[0]?.size <= 1 * 1024 * 1024, "Size shouldn't be more than 1 MB")

have you thought about doing it this way:

const fileSchema = z
    .instanceof( FileList )
    .refine(
        files => [ 'image/jpg', 'image/jpeg', 'image/png' ].includes( files?.[ 0 ]?.type ),
        'Accepted Formats: JPG/JPEG/PNG'
    )
    .refine( files => files?.[ 0 ]?.size <= 1 * 1024 * 1024, "Size shouldn't be more than 1 MB" )
Mojtaba-NA commented 1 year ago

I wasn't aware of .instanceof, this seems more elegant. Thank you.

Mojtaba-NA commented 1 year ago
const fileSchema = z
  .custom<FileList>(files => files instanceof FileList)
  .refine(
    files => ['image/jpg', 'image/jpeg', 'image/png'].includes(files?.[0]?.type),
    'Accepted Formats: JPG/JPEG/PNG'
  )
  .refine(files => files?.[0]?.size <= 1 * 1024 * 1024, "Size shouldn't be more than 1 MB")

have you thought about doing it this way:

const fileSchema = z
    .instanceof( FileList )
    .refine(
        files => [ 'image/jpg', 'image/jpeg', 'image/png' ].includes( files?.[ 0 ]?.type ),
        'Accepted Formats: JPG/JPEG/PNG'
    )
    .refine( files => files?.[ 0 ]?.size <= 1 * 1024 * 1024, "Size shouldn't be more than 1 MB" )

Now I remember why I do it like that, "FileList" is only available in browser so using instanceof would throw a reference error in a framework like nextjs but in nestjs, I will be using the way you recommended (instanceof).

pshaddel commented 1 year ago

We use it for validating IBAN or country code:

import { isValid } from 'iban';
import { z } from 'zod';
const iban = z.custom<string>((val) => {
  return isValid(val as string);
});
type Account = z.infer<typeof iban>;
iban.parse('BE539007547034'); // 'BE539007547034'
iban.parse('123456789123456789'); // throws;
stale[bot] commented 1 year ago

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.

kalnode commented 7 months ago

I have a zod validator with an existing type like so.

It works, seemingly, but I'm sure this is not resilient; would be nice if someone can clean this up.

type ExistingTypeA = {
    id: string
    SomeNestedProperty: Array<ExistingTypeB>
}

MyValidatorThing: zod.custom<ExistingTypeA>( (value:any) => value && value.SomeNestedProperty && value.SomeNestedProperty.some( (x:ExistingTypeB) => { return x.SomeKey === 'SomeValue' }))

Notes:

1 - I don't like the value:any. I would like to of course use value:ExistingTypeA but then I get an IDE TS warning:

Argument of type '(value: ExistingTypeA) => boolean' is not assignable to parameter of type '(data: unknown) => any'.
  Types of parameters 'value' and 'data' are incompatible.
    Type 'unknown' is not assignable to type 'ExistingTypeA'.ts(2345)

... so, using value:any to pass that hurdle in the meantime.

2 - Consequently, when the validator (MyValidatorThing) triggers in the browser (runtime), errors occur in the console: " "value is not found" if input is empty, or "value.SomeNestedProperty not found" if attempting to submit an object of an entirely different type.

To clear these runtime errors, I added the extra value && and value.SomeNestedProperty && which seems to account for these cases (no-value or wrong-value-object-type).


In testing, so far this validator works and I get my custom logic (checking a nested property).

I'm sure this isn't rock-solid though, esp with an :any in there.