apps/mantine.dev/src/pages/form/schema-validation.mdx
import { Layout } from '@/layout'; import { MDX_DATA } from '@/mdx';
export default Layout(MDX_DATA.formSchemaValidation);
@mantine/form has built-in support for Standard Schema –
a community specification implemented by many popular schema validation libraries including
zod, valibot,
and arktype. Use schemaResolver to validate
forms with any Standard Schema-compliant library without extra resolver packages.
Full list of supported libraries
If you do not know which schema validation library to choose, use zod – it is the most modern and developer-friendly library.
By default, schemaResolver returns a function that may return a Promise, since the
Standard Schema specification allows async validation. Pass { sync: true } when you know
your schema is synchronous (e.g., Zod, Valibot) to get synchronous return types for
form.validate(), form.validateField(), and form.isValid().
Installation:
<InstallScript packages="zod" />Basic field validation:
import { z } from 'zod/v4';
import { useForm, schemaResolver } from '@mantine/form';
const schema = z.object({
name: z.string().min(2, { error: 'Name should have at least 2 letters' }),
email: z.email({ error: 'Invalid email' }),
age: z.number().min(18, { error: 'You must be at least 18 to create an account' }),
});
const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
email: '',
age: 16,
},
validate: schemaResolver(schema, { sync: true }),
});
form.validate();
form.errors;
// -> {
// name: 'Name should have at least 2 letters',
// email: 'Invalid email',
// age: 'You must be at least 18 to create an account'
// }
Nested field validation:
import { z } from 'zod/v4';
import { useForm, schemaResolver } from '@mantine/form';
const nestedSchema = z.object({
nested: z.object({
field: z.string().min(2, { error: 'Field should have at least 2 letters' }),
}),
});
const form = useForm({
mode: 'uncontrolled',
initialValues: {
nested: {
field: '',
},
},
validate: schemaResolver(nestedSchema, { sync: true }),
});
form.validate();
form.errors;
// -> {
// 'nested.field': 'Field should have at least 2 letters',
// }
List field validation:
import { z } from 'zod/v4';
import { useForm, schemaResolver } from '@mantine/form';
const listSchema = z.object({
list: z.array(
z.object({
name: z.string().min(2, { error: 'Name should have at least 2 letters' }),
})
),
});
const form = useForm({
mode: 'uncontrolled',
initialValues: {
list: [{ name: '' }],
},
validate: schemaResolver(listSchema, { sync: true }),
});
form.validate();
form.errors;
// -> {
// 'list.0.name': 'Name should have at least 2 letters',
// }
Async validation – use schemaResolver without { sync: true } when your schema
includes async checks (e.g., checking if an email is already taken via an API call).
In this case, form.validate(), form.validateField(), and form.isValid() return promises:
import { z } from 'zod/v4';
import { useForm, schemaResolver } from '@mantine/form';
const schema = z
.object({
email: z.email({ error: 'Invalid email' }),
})
.refine(
async (data) => {
const isTaken = await checkEmailExists(data.email);
return !isTaken;
},
{ error: 'Email is already taken', path: ['email'] }
);
const form = useForm({
mode: 'uncontrolled',
initialValues: { email: '' },
validate: schemaResolver(schema),
});
// form.validate() returns a Promise
await form.validate();