T
TanStack5mo ago
fascinating-indigo

What is the correct way to validate image files using zod validator?

I tried doing something like this:
const ACCEPTED_IMAGE_TYPES = ['jpeg', 'jpg', 'png', 'webp'];
const formSchema = z.object({

logo: z
.any()
.refine((files) => {
return files?.[0]?.size <= MAX_FILE_SIZE;
}, `Max image size is 5MB.`)
.refine(
files => ACCEPTED_IMAGE_MIME_TYPES.includes(files?.[0]?.type),
'Only .jpg, .jpeg, .png and .webp formats are supported.',
),
});
const form = useAppForm({
defaultValues: {

logo: null,
},
validators: {
onSubmit: formSchema, //---> type error here
},
});
const ACCEPTED_IMAGE_TYPES = ['jpeg', 'jpg', 'png', 'webp'];
const formSchema = z.object({

logo: z
.any()
.refine((files) => {
return files?.[0]?.size <= MAX_FILE_SIZE;
}, `Max image size is 5MB.`)
.refine(
files => ACCEPTED_IMAGE_MIME_TYPES.includes(files?.[0]?.type),
'Only .jpg, .jpeg, .png and .webp formats are supported.',
),
});
const form = useAppForm({
defaultValues: {

logo: null,
},
validators: {
onSubmit: formSchema, //---> type error here
},
});
Type 'ZodObject<{ logo: ZodEffects<ZodEffects<ZodAny, any, any>, any, any>; }, "strip", ZodTypeAny, { ...; }, { ...; }>' is not assignable to type 'FormValidateOrFn<{ logo: null; }> | undefined'.
Type 'ZodObject<{ logo: ZodEffects<ZodEffects<ZodAny, any, any>, any, any>; }, "strip", ZodTypeAny, { ...; }, { ...; }>' is not assignable to type 'FormValidateOrFn<{ logo: null; }> | undefined'.
Can't seems to find a solution to this error.
20 Replies
fascinating-indigo
fascinating-indigo5mo ago
what version of zod are you using? you must be 3.24 or higher to have standard schema compatability also, zod should be able to support z.instanceof(File) which would make your example file validation a bit easier
fascinating-indigo
fascinating-indigoOP5mo ago
I am using version ^3.24.2 I tried that too It doesn't work
fascinating-indigo
fascinating-indigo5mo ago
then perhaps your typescript is not updated? Tanstack form requires v5.4 upwards the code you provided works for me
fascinating-indigo
fascinating-indigoOP5mo ago
You don't get a type error in this code? I am using typescript v5.6.3.
fascinating-indigo
fascinating-indigo5mo ago
I guess it leaves only one package in question. What's your tanstack form version? if it's up to date, then you might need to explicitly type defaultValues with the schema's input type type FormSchemaType = z.input<typeof formSchema>;
defaultValues: {
logo: null
} as FormSchemaType
defaultValues: {
logo: null
} as FormSchemaType
fascinating-indigo
fascinating-indigoOP5mo ago
That worked i think Can you tell me why this is needed when having file inputs?
fascinating-indigo
fascinating-indigo5mo ago
because it may interpret your defaultValues as being stricter than you intended. In your case, logo is a nullable File, but your defaultValues can be interpreted as { logo: null }. Typescript would then complain that a nullable File is not assignable to null.
fascinating-indigo
fascinating-indigoOP5mo ago
Thanks
fascinating-indigo
fascinating-indigoOP5mo ago
Do you have any idea how to solve this?
No description
fascinating-indigo
fascinating-indigoOP5mo ago
This is the filefield i am using with my appform I add it to my appform like:
export const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField,
FileField,
},
formComponents: {
SubmitButton,
},
});
export const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: {
TextField,
FileField,
},
formComponents: {
SubmitButton,
},
});
fascinating-indigo
fascinating-indigo5mo ago
an HTML input's value property must be a string. Read up on it in the MDN Web Docs
MDN Web Docs
- HTML: HyperText Markup Language | MDN
elements with type="file" let the user choose one or more files from their device storage. Once chosen, the files can be uploaded to a server using form submission, or manipulated using JavaScript code and the File API.
fascinating-indigo
fascinating-indigo5mo ago
but generally, controlled inputs of type="file" are a bit tricky in React. There's some stack overflow answers like this one that go into more detail.
fascinating-indigo
fascinating-indigoOP5mo ago
Thanks, i will check it out.
conscious-sapphire
conscious-sapphire5mo ago
Having a similar issue (screenshot below). Seems like this should work based on the discussion above. (Using typescript^5.8.2, zod^3.24.1, @tanstack/react-form^1.0.5) However, I'm getting Type 'null' is not comparable to type 'File'.ts(2352). Which totally makes sense. Workaround is to cast (as unknown as SchemaType) Aside from file types, this seems like a general issue where the initial state of the form doesn't necessarily have the same type as the value that needs to be validated (e.g. initial values may be null or "" but we want to require some type at validation/submission time). Are there any good patterns to work around this?
No description
fascinating-indigo
fascinating-indigo5mo ago
for nullable inputs (but non-nullable outputs), you can use refinements. Here's my snippet for it
/**
* Modifies the provided schema to be nullable on input, but non-nullable on output.
*/
export function nullableInput<TSchema extends ZodTypeAny>(schema: TSchema) {
return schema.nullable().transform((value, ctx: RefinementCtx) => {
if (value === null) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_type,
expected: z.getParsedType(schema),
received: z.ZodParsedType.null,
});
return z.NEVER;
}
return value;
});
}

nullableInput(z.instanceof(File))
/**
* Modifies the provided schema to be nullable on input, but non-nullable on output.
*/
export function nullableInput<TSchema extends ZodTypeAny>(schema: TSchema) {
return schema.nullable().transform((value, ctx: RefinementCtx) => {
if (value === null) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_type,
expected: z.getParsedType(schema),
received: z.ZodParsedType.null,
});
return z.NEVER;
}
return value;
});
}

nullableInput(z.instanceof(File))
you'll have to parse it once more in onSubmit, but the returned value will work nicely for that the type would also be changed to z.input<typeof fileSchema>
conscious-sapphire
conscious-sapphire5mo ago
interesting. I don't think I understand the difference between z.infer vs z.input vs z.output
fascinating-indigo
fascinating-indigo5mo ago
essentially, Zod (and standard schemas) have the concept of input types and output types. Consider the example above. We use transform to turn a nullable value into a non-nullable one. That means z.input will be what it expects as input (nullable) and z.output will be what comes out after all the zod effects and transforms (not nullable) as for z.infer, I'm not sure which one it picks. That's why I tend to avoid it. tanstack form exclusively works with the input variant of standard schemas. That includes the value that is passed to onSubmit, which is why you have to parse it once more to receive the output
conscious-sapphire
conscious-sapphire5mo ago
ok that makes sense, thank you i see what you mean about re-validating in onSubmit (value.file type is File | null). it's not ideal to do extra runtime work to satisfy a compiler condition that should never occur. maybe casting to as unkown as SchemaType isn't too bad
fascinating-indigo
fascinating-indigo5mo ago
not quite. Remember, transforms can go beyond just checking if it‘s non-nullable. You can put your form-values-to-server-request-dto functionality in the transform as well, which would no longer satisfy that condition also, zod is quite a fast library, those couple ms you spend rerunning validation vs. the 1.5 seconds of server request is irrelevant
conscious-sapphire
conscious-sapphire5mo ago
Sure you could waive away a couple ms, but it also messes up the DX. In the onSubmit callback, validation is already supposed to have passed, right? However, the onSubmit callback seems to be typed with z.input instead of z.output. Because of that, now in some circumstances we have to re-run validation in onSubmit. So ultimately the nullableInput feels like just as much as a hack as casting to unknown. Might as well just do the hack at compile time instead of runtime.

Did you find this page helpful?