Validating on the server
The schema a definition builds is a plain Valibot schema, so it validates on a server just like in the browser. Never trust what a request sends: run it through the same schema the form used. Below you will learn how, and what changes when the value travels as JSON.
Parse the request
Build the schema from the same definition the form rendered, with a builder that has the same options, and parse the request's body with v.safeParse:
// The client's builder options: extensions, types, rules and messages.
const builder = createSchemaBuilder({ extensions: [affects()] })
const schema = builder.build(definition)
function handleSignup(body: unknown) {
const result = v.safeParse(schema, body)
if (!result.success) {
// { email: ['Please enter your email'] }, by the dot path of each field
return {
status: 422,
errors: v.flatten<typeof schema>(result.issues).nested,
}
}
return { status: 201, output: result.output }
}- Use the same builder options as the client: the same extensions, field types, rules and messages. Without the affects extension, for example, a definition with conditions fails to build.
- Use the same definition. When people edit your definitions, store each version, and validate a request against the one the form showed, for example by an id the form sends along.
- Keys the definition doesn't know are dropped from the output, as
v.objectdoes. - Form rules and conditions run on the server too. A hidden field isn't validated. A field that
hideAndClearhides outputs nothing, and one hidden otherwise outputs its value when it is valid. - Build the schema once for each definition, and reuse it for every request.
Missing keys
Formisch always sends every key of the form, but a request from somewhere else may leave one out. A required field whose key is missing reports its required message, the same as one that is undefined:
handleSignup({}) // 422, email: 'Please enter your email'Before 1.0, it reported Valibot's own Invalid key: Expected "email" but received undefined. See migrating to 1.0.
Dates
A date or datetime with "output": "date" outputs a Date. JSON has no dates, so JSON.stringify writes it as a full timestamp, which the field rejects on the server:
const output = v.parse(schema, {
email: 'jane@example.com',
birthDate: '2000-01-15',
})
// output.birthDate is a Date. JSON.stringify writes it as a timestamp,
// like "2000-01-14T23:00:00.000Z", which a `date` rejects.
const fromOutput = handleSignup(JSON.parse(JSON.stringify(output))) // 422
const fromInput = handleSignup({
email: 'jane@example.com',
birthDate: '2000-01-15',
}) // 201So send the form's input, not its output. Formisch's getInput(form) gives it, and the server's parse makes the same output from it. Or leave out "output": "date" in forms that go to a JSON API, and turn the ISO string into a date on the server.
The same goes for any output that JSON changes: validate the value the schema takes, never the one it gave.
Files
A file holds a File, which JSON can't carry. Send the request as multipart/form-data instead: the input as JSON in one part, and each file in a part of its own. On the server, put the files back where the definition has them, and parse:
// In the browser: the form's input, from Formisch's getInput(form), as
// JSON in one part, and each file in a part of its own.
const formInput = {
email: 'jane@example.com',
cv: new File(['%PDF'], 'cv.pdf', { type: 'application/pdf' }),
}
const { cv, ...rest } = formInput
const body = new FormData()
body.append('input', JSON.stringify(rest))
body.append('cv', cv)
const request = new Request('https://example.com/signup', {
method: 'POST',
body,
})
// On the server: put the files back in place, then parse.
const data = await request.formData()
const input = {
...JSON.parse(String(data.get('input'))),
cv: data.get('cv') ?? undefined,
}
const withFile = handleSignup(input) // 201- Node 20 and later have
File,FormDataandRequestas globals, and most servers give a request'sformData(). The schema reads theFileglobal while it parses. - For a field with
"multiple": true, append each file under the same name, and read them withdata.getAll(name). - A
Fileonly knows the MIME type the browser gave it.acceptand the file rules check that and the size, not the content, so check the content of a file on your server before you trust it.
Next steps
See supported versions for the Node versions the packages run on, and checking definitions to check a definition before you store it.