Form schema validation

use-form schema based validation with zod, yup, joi and superstruct

Schema based validation

@mantine/form supports schema validation with:

You need to install one of the libraries yourself, @mantine/form package does not depend on any of them. If you do not know what schema validation library to choose, use zod, it is the most modern and developer-friendly library.

zod

Installation:

yarn add zod mantine-form-zod-resolver

Basic fields validation with zod v3:

import { zodResolver } from 'mantine-form-zod-resolver'; import { z } from 'zod'; import { useForm } from '@mantine/form'; const schema = z.object({ name: z .string() .min(2, { message: 'Name should have at least 2 letters' }), email: z.string().email({ message: 'Invalid email' }), age: z.number().min(18, { message: 'You must be at least 18 to create an account', }), }); const form = useForm({ mode: 'uncontrolled', initialValues: { name: '', email: '', age: 16, }, validate: zodResolver(schema), }); 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 fields validation

import { zodResolver } from 'mantine-form-zod-resolver'; import { z } from 'zod'; import { useForm } from '@mantine/form'; const nestedSchema = z.object({ nested: z.object({ field: z .string() .min(2, { message: 'Field should have at least 2 letters' }), }), }); const form = useForm({ mode: 'uncontrolled', initialValues: { nested: { field: '', }, }, validate: zodResolver(nestedSchema), }); form.validate(); form.errors; // -> { // 'nested.field': 'Field should have at least 2 letters', // }

List fields validation:

import { zodResolver } from 'mantine-form-zod-resolver'; import { z } from 'zod'; import { useForm } from '@mantine/form'; const listSchema = z.object({ list: z.array( z.object({ name: z .string() .min(2, { message: 'Name should have at least 2 letters' }), }) ), }); const form = useForm({ mode: 'uncontrolled', initialValues: { list: [{ name: '' }], }, validate: zodResolver(listSchema), }); form.validate(); form.errors; // -> { // 'list.0.name': 'Name should have at least 2 letters', // }

zod v4

To use zod 4:

  • Update mantine-form-zod-resolver to 1.2.0 or later version
  • Update zod to version 3.25.0 or later
  • Replace zod imports with zod/v4 (only if you have zod@3 in your package.json)
  • Replace zodResolver with zod4Resolver in your code
  • All other code remains the same

Example with zod v4:

import { z } from 'zod/v4'; import { zod4Resolver } from 'mantine-form-zod-resolver'; 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({ initialValues: { name: '', email: '', age: 16, }, validate: zod4Resolver(schema), })

yup

Installation:

yarn add yup mantine-form-yup-resolver

Basic fields validation:

import { yupResolver } from 'mantine-form-yup-resolver'; import * as yup from 'yup'; import { useForm } from '@mantine/form'; const schema = yup.object().shape({ name: yup.string().min(2, 'Name should have at least 2 letters'), email: yup .string() .required('Invalid email') .email('Invalid email'), age: yup .number() .min(18, 'You must be at least 18 to create an account'), }); const form = useForm({ mode: 'uncontrolled', initialValues: { name: '', email: '', age: 16, }, validate: yupResolver(schema), }); 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 fields validation:

import { yupResolver } from 'mantine-form-yup-resolver'; import * as yup from 'yup'; import { useForm } from '@mantine/form'; const nestedSchema = yup.object().shape({ nested: yup.object().shape({ field: yup .string() .min(2, 'Field should have at least 2 letters'), }), }); const form = useForm({ mode: 'uncontrolled', initialValues: { nested: { field: '', }, }, validate: yupResolver(nestedSchema), }); form.validate(); form.errors; // -> { // 'nested.field': 'Field should have at least 2 letters', // }

List fields validation:

import { yupResolver } from 'mantine-form-yup-resolver'; import * as yup from 'yup'; import { useForm } from '@mantine/form'; const listSchema = yup.object().shape({ list: yup.array().of( yup.object().shape({ name: yup .string() .min(2, 'Name should have at least 2 letters'), }) ), }); const form = useForm({ mode: 'uncontrolled', initialValues: { list: [{ name: '' }], }, validate: yupResolver(listSchema), }); form.validate(); form.errors; // -> { // 'list.0.name': 'Name should have at least 2 letters', // }

joi

Installation:

yarn add joi mantine-form-joi-resolver

Basic fields validation:

import Joi from 'joi'; import { joiResolver } from 'mantine-form-joi-resolver'; import { useForm } from '@mantine/form'; const schema = Joi.object({ name: Joi.string().min(2).messages({ 'string.min': 'Name should have at least 2 letters', 'string.empty': 'Name should have at least 2 letters', }), email: Joi.string() .email({ tlds: { allow: false } }) .messages({ 'string.email': 'Invalid email', 'string.empty': 'Invalid email', }), age: Joi.number() .min(18) .message('You must be at least 18 to create an account'), }); const form = useForm({ mode: 'uncontrolled', initialValues: { name: '', email: '', age: 16, }, validate: joiResolver(schema), }); 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 fields validation:

import Joi from 'joi'; import { joiResolver } from 'mantine-form-joi-resolver'; import { useForm } from '@mantine/form'; const nestedSchema = Joi.object({ nested: Joi.object({ field: Joi.string().min(2).messages({ 'string.min': 'Field should have at least 2 letters', 'string.empty': 'Field should have at least 2 letters', }), }), }); const form = useForm({ mode: 'uncontrolled', initialValues: { nested: { field: '', }, }, validate: joiResolver(nestedSchema), }); form.validate(); form.errors; // -> { // 'nested.field': 'Field should have at least 2 letters', // }

List fields validation:

import Joi from 'joi'; import { joiResolver } from 'mantine-form-joi-resolver'; import { useForm } from '@mantine/form'; const listSchema = Joi.object({ list: Joi.array().items( Joi.object({ name: Joi.string().min(2).messages({ 'string.min': 'Name should have at least 2 letters', 'string.empty': 'Name should have at least 2 letters', }), }) ), }); const form = useForm({ mode: 'uncontrolled', initialValues: { list: [{ name: '' }], }, validate: joiResolver(listSchema), }); form.validate(); form.errors; // -> { // 'list.0.name': 'Name should have at least 2 letters', // }

superstruct

Installation:

yarn add superstruct mantine-form-superstruct-resolver

Basic fields validation:

import isEmail from 'is-email'; import { superstructResolver } from 'mantine-form-superstruct-resolver'; import * as s from 'superstruct'; const emailString = s.define('email', isEmail); const schema = s.object({ name: s.size(s.string(), 2, 30), email: emailString, age: s.min(s.number(), 18), }); const form = useForm({ mode: 'uncontrolled', initialValues: { name: '', email: '', age: 16, }, validate: superstructResolver(schema), }); form.validate(); form.errors; // -> { // name: 'name: Expected a string with a length between `2` and `30` but received one with a length of `0`', // email: 'email: Expected a value of type `email`, but received: `""`', // age: 'age: Expected a number greater than or equal to 18 but received `16`', // }

Nested fields validation:

import { superstructResolver } from 'mantine-form-superstruct-resolver'; import * as s from 'superstruct'; import { useForm } from '@mantine/form'; const nestedSchema = s.object({ nested: s.object({ field: s.size(s.string(), 2, 30), }), }); const form = useForm({ mode: 'uncontrolled', initialValues: { nested: { field: '', }, }, validate: superstructResolver(nestedSchema), }); form.validate(); form.errors; // -> { // 'nested.field': 'nested field: Expected a string with a length between `2` and `30` but received one with a length of `0`', // }

List fields validation:

import { superstructResolver } from 'mantine-form-superstruct-resolver'; import * as s from 'superstruct'; import { useForm } from '@mantine/form'; const listSchema = s.object({ list: s.array( s.object({ name: s.size(s.string(), 2, 30), }) ), }); const form = useForm({ mode: 'uncontrolled', initialValues: { list: [{ name: '' }], }, validate: superstructResolver(listSchema), }); form.validate(); form.errors; // -> { // 'list 0 name: Expected a string with a length between `2` and `30` but received one with a length of `0`', // }

valibot

Installation:

yarn add valibot mantine-form-valibot-resolver

Basic fields validation:

import { valibotResolver } from 'mantine-form-valibot-resolver'; import * as v from 'valibot'; import { useForm } from '@mantine/form'; const schema = v.object({ name: v.pipe( v.string(), v.minLength(2, 'Name should have at least 2 letters') ), email: v.pipe(v.string(), v.email('Invalid email')), age: v.pipe( v.number(), v.minValue(18, 'You must be at least 18 to create an account') ), }); const form = useForm({ initialValues: { name: '', email: '', age: 16, }, validate: valibotResolver(schema), }); 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 fields validation:

import { valibotResolver } from 'mantine-form-valibot-resolver'; import * as v from 'valibot'; import { useForm } from '@mantine/form'; const nestedSchema = v.object({ nested: v.object({ field: v.pipe( v.string(), v.minLength(2, 'Field should have at least 2 letters') ), }), }); const form = useForm({ initialValues: { nested: { field: '', }, }, validate: valibotResolver(nestedSchema), }); form.validate(); form.errors; // -> { // 'nested.field': 'Field should have at least 2 letters', // }

List fields validation:

import { valibotResolver } from 'mantine-form-valibot-resolver'; import * as v from 'valibot'; import { useForm } from '@mantine/form'; const listSchema = v.object({ list: v.array( v.object({ name: v.pipe( v.string(), v.minLength(2, 'Name should have at least 2 letters') ), }) ), }); const form = useForm({ initialValues: { list: [{ name: '' }], }, validate: valibotResolver(listSchema), }); form.validate(); form.errors; // -> { // 'list.0.name': 'Name should have at least 2 letters', // }

With TypeScript:

You can use the InferInput type from the valibot library to get the type of the form data.

import { valibotResolver } from 'mantine-form-valibot-resolver'; import * as v from 'valibot'; import { useForm } from '@mantine/form'; export const userSchema = v.object({ email: v.pipe(v.string(), v.email()), }); type FormData = v.InferInput<typeof userSchema>; const form = useForm<FormData>({ initialValues: { email: '', }, validate: valibotResolver(userSchema), });