T
TanStack3mo ago
quickest-silver

uestion: Coercing FormData values for server-side Zod validation

Hey everyone! I’m using Zod for validation both on the client and server. On the client, validation works perfectly. But when I send the form to a server action using FormData, things fall apart. Specifically: The browser correctly sends all form fields as strings, but my server-side schema expects num to be a number:
export const simpleFormSchema = z.object({
num: z.number().int()
});
export const simpleFormSchema = z.object({
num: z.number().int()
});
In my server action, I use createServerValidate() like this:
export async function writeReviewAction(prev: unknown, formData: FormData) {
try {
const { num } = await serverValidate(formData);
console.log('great success');
} catch (error) {
if (error instanceof ServerValidateError) {
return error.formState;
}
throw error;
}
}
export async function writeReviewAction(prev: unknown, formData: FormData) {
try {
const { num } = await serverValidate(formData);
console.log('great success');
} catch (error) {
if (error instanceof ServerValidateError) {
return error.formState;
}
throw error;
}
}
But since formData.get("num") returns a string, z.number() fails validation. What’s the proper way to coerce FormData values (like "num") into the correct types before passing them into the schema for server validation? Should I transform formData to a plain object with type coercion first, or is there a built-in/best-practice way of handling this with Zod or createServerValidate? Thanks in advance!
8 Replies
quickest-silver
quickest-silverOP3mo ago
I have tried the following ofcourse:
export const simpleFormSchema = z.object({
num: z.coerce.number().int()
})
export const simpleFormSchema = z.object({
num: z.coerce.number().int()
})
though this results in a type error. something allong the lines of ~standard types are incompatible between these types Type 'Types<{num:unknown}> type unknown is not assignable to type number
fascinating-indigo
fascinating-indigo3mo ago
Haven't worked with Server-Side, but: TSF doesn't transform the data, you've got to do that yourself.
quickest-silver
quickest-silverOP3mo ago
thanks for your reply? Any idea how i would tranform the data in the server action?
quickest-silver
quickest-silverOP3mo ago
I noticed tin the following example that the result of validatedData.age is also a string and not a number; https://tanstack.com/form/latest/docs/framework/react/examples/next-server-actions?path=examples%2Freact%2Fnext-server-actions%2Fsrc%2Fapp%2Faction.ts
React TanStack Form Next Server Actions Example | TanStack Form Docs
An example showing how to implement Next Server Actions in React using TanStack Form.
fascinating-indigo
fascinating-indigo3mo ago
You'll have to z.parse somewhere, not sure where though, the output of it will be the transformer/coerced values
quickest-silver
quickest-silverOP3mo ago
the weird thing is (or perhaps it's my misunderstanding) that serverValidate takes in formData, so the input values values would always be strings, so I really don't know where I'd do the transform
fascinating-indigo
fascinating-indigo3mo ago
You should validate the data on the server again as you can't trust the data the client sends you. You'll need to parse the data again here
// someAction

const validatedData = await serverValidate(formData)
// someAction

const validatedData = await serverValidate(formData)
This should still be the z.input type. So throw that through z.parse again and you have the transformer data (z.output) Using coerce you should be able to only need one Schema that can handle the incoming values on the client and the server
quickest-silver
quickest-silverOP3mo ago
The problem is that the validation fails. await serverValidate(formData) throws a validation error since
typeof formdata.get("num") === "string"
typeof formdata.get("num") === "string"
and transforming formData before passing it to serverValidate is not allowed, since that would not fit the FormData type i did find a somewhat sketchy solution; this works, though I feel like this is not the proper way to go:
const serverSchema = clientSchema
.omit({
num: true,
})
.extend({
num: z.coerce.number().int(),
});

const serverValidate = createServerValidate({
...formOpts,
onServerValidate: async ({ value }) => {
return standardSchemaValidators.validateAsync(
{ value, validationSource: 'form' },
serverSchema,
);
},
});
const serverSchema = clientSchema
.omit({
num: true,
})
.extend({
num: z.coerce.number().int(),
});

const serverValidate = createServerValidate({
...formOpts,
onServerValidate: async ({ value }) => {
return standardSchemaValidators.validateAsync(
{ value, validationSource: 'form' },
serverSchema,
);
},
});

Did you find this page helpful?