T
TanStack6mo ago
deep-jade

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
deep-jade
deep-jadeOP6mo 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
equal-jade
equal-jade6mo ago
Haven't worked with Server-Side, but: TSF doesn't transform the data, you've got to do that yourself.
deep-jade
deep-jadeOP6mo ago
thanks for your reply? Any idea how i would tranform the data in the server action?
deep-jade
deep-jadeOP6mo 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.
equal-jade
equal-jade6mo ago
You'll have to z.parse somewhere, not sure where though, the output of it will be the transformer/coerced values
deep-jade
deep-jadeOP6mo 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
equal-jade
equal-jade6mo 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
deep-jade
deep-jadeOP6mo 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?