Performant, flexible and extensible forms with easy to use validation.
This function allows you to use any external validation library such as Yup, Zod, Joi, Vest, Ajv and many others. The goal is to make sure you can seamlessly integrate whichever validation library you prefer. If you're not using a library, you can always write your own logic to validate your forms.
Install your preferred validation library alongside @hookform/resolvers.
npm install @hookform/resolvers # npm
yarn add @hookform/resolvers # yarn
pnpm install @hookform/resolvers # pnpm
bun install @hookform/resolvers # bun
Resolver Comparison
| resolver | Infer values from schema | criteriaMode |
|---|---|---|
| AJV | ❌ | firstError | all |
| ata-validator | ❌ | firstError | all |
| Arktype | ✅ | firstError |
| class-validator | ✅ | firstError | all |
| computed-types | ✅ | firstError |
| Effect | ✅ | firstError | all |
| fluentvalidation-ts | ❌ | firstError |
| io-ts | ✅ | firstError |
| joi | ❌ | firstError | all |
| Nope | ❌ | firstError |
| Standard Schema | ✅ | firstError | all |
| Superstruct | ✅ | firstError |
| typanion | ✅ | firstError |
| typebox | ✅ | firstError | all |
| typeschema | ❌ | firstError | all |
| valibot | ✅ | firstError | all |
| vest | ❌ | firstError | all |
| vine | ✅ | firstError | all |
| yup | ✅ | firstError | all |
| zod | ✅ | firstError | all |
Most of the resolvers can infer the output type from the schema. See comparison table for more details.
useForm<Input,Context,Output>()Example:
import{useForm}from'react-hook-form';import{zodResolver}from'@hookform/resolvers/zod';import{z}from'zod';// or 'zod/v4'constschema=z.object({id: z.number(),});// Automatically infers the output type from the schemauseForm({resolver: zodResolver(schema),});// Force the output typeuseForm<z.input<typeofschema>,any,z.output<typeofschema>>({resolver: zodResolver(schema),});type Options = {
mode: 'async' | 'sync',
raw?: boolean
}
resolver(schema: object, schemaOptions?: object, resolverOptions: Options)
| type | Required | Description | |
|---|---|---|---|
| schema | object | ✓ | validation schema |
| schemaOptions | object | validation library schema options | |
| resolverOptions | object | resolver options, async is the default mode |
Dead simple Object schema validation.
⚠️ Pass context viauseForm({ context }), not viayupResolver'sschemaOptions.schemaOptions.contextis overridden by the form context, so use theuseFormcontext object instead.
// CorrectuseForm({resolver: yupResolver(schema),context: {foo: true},});// Avoid - schemaOptions.context will be ignored/overriddenyupResolver(schema,{context: {foo: true}});import{useForm}from'react-hook-form';import{yupResolver}from'@hookform/resolvers/yup';import*asyupfrom'yup';constschema=yup.object().shape({name: yup.string().required(),age: yup.number().required(),}).required();constApp=()=>{const{ register, handleSubmit }=useForm({resolver: yupResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('name')}/><inputtype="number"{...register('age')}/><inputtype="submit"/></form>);};TypeScript-first schema validation with static type inference
⚠️ Example below uses thevalueAsNumber, which requiresreact-hook-formv6.12.0 (released Nov 28, 2020) or later.
import{useForm}from'react-hook-form';import{zodResolver}from'@hookform/resolvers/zod';import{z}from'zod';// or 'zod/v4'constschema=z.object({name: z.string().min(1,{message: 'Required'}),age: z.number().min(10),});constApp=()=>{const{
register,
handleSubmit,formState: { errors },}=useForm({resolver: zodResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('name')}/>{errors.name?.message&&<p>{errors.name?.message}</p>}<inputtype="number"{...register('age',{valueAsNumber: true})}/>{errors.age?.message&&<p>{errors.age?.message}</p>}<inputtype="submit"/></form>);};
⚠️ If your schema uses.default(...)on a field, that field becomes optional on the schema's input type (z.input) but stays required on its output type (z.output/z.infer) — the default only fills it in after validation. Passing a single generic touseForm<T>pins both to the same type and will conflict withzodResolver, which infers input and output separately. Either omit the generic and let it infer fromresolver, or specify all three explicitly:constschema=z.object({debug_mode: z.boolean().default(true)});useForm<z.input<typeofschema>,unknown,z.output<typeofschema>>({resolver: zodResolver(schema),});
A simple and composable way to validate data in JavaScript (or TypeScript).
import{useForm}from'react-hook-form';import{superstructResolver}from'@hookform/resolvers/superstruct';import{object,string,number}from'superstruct';constschema=object({name: string(),age: number(),});constApp=()=>{const{ register, handleSubmit }=useForm({resolver: superstructResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('name')}/><inputtype="number"{...register('age',{valueAsNumber: true})}/><inputtype="submit"/></form>);};The most powerful data validation library for JS.
import{useForm}from'react-hook-form';import{joiResolver}from'@hookform/resolvers/joi';importJoifrom'joi';constschema=Joi.object({name: Joi.string().required(),age: Joi.number().required(),});constApp=()=>{const{ register, handleSubmit }=useForm({resolver: joiResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('name')}/><inputtype="number"{...register('age')}/><inputtype="submit"/></form>);};Vest 🦺 Declarative Validation Testing.
import{useForm}from'react-hook-form';import{vestResolver}from'@hookform/resolvers/vest';import{create,test,enforce}from'vest';constvalidationSuite=create((data={})=>{test('username','Username is required',()=>{enforce(data.username).isNotEmpty();});test('password','Password is required',()=>{enforce(data.password).isNotEmpty();});});constApp=()=>{const{ register, handleSubmit }=useForm({resolver: vestResolver(validationSuite),});return(<formonSubmit={handleSubmit((data)=>console.log(data))}><input{...register('username')}/><inputtype="password"{...register('password')}/><inputtype="submit"/></form>);};Decorator-based property validation for classes.
⚠️ Remember to add these options to yourtsconfig.json!
"strictPropertyInitialization": false,
"experimentalDecorators": true
import{useForm}from'react-hook-form';import{classValidatorResolver}from'@hookform/resolvers/class-validator';import{Length,Min,IsEmail}from'class-validator';classUser{
@Length(2,30)username: string;
@IsEmail()email: string;}constresolver=classValidatorResolver(User);constApp=()=>{const{
register,
handleSubmit,formState: { errors },}=useForm<User>({ resolver });return(<formonSubmit={handleSubmit((data)=>console.log(data))}><inputtype="text"{...register('username')}/>{errors.username&&<span>{errors.username.message}</span>}<inputtype="text"{...register('email')}/>{errors.email&&<span>{errors.email.message}</span>}<inputtype="submit"value="Submit"/></form>);};Validate your data with powerful decoders.
importReactfrom'react';import{useForm}from'react-hook-form';import{ioTsResolver}from'@hookform/resolvers/io-ts';importtfrom'io-ts';// you don't have to use io-ts-types, but it's very usefulimportttfrom'io-ts-types';constschema=t.type({username: t.string,age: tt.NumberFromString,});constApp=()=>{const{ register, handleSubmit }=useForm({resolver: ioTsResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('username')}/><inputtype="number"{...register('age')}/><inputtype="submit"/></form>);};exportdefaultApp;A small, simple, and fast JS validator
import{useForm}from'react-hook-form';import{nopeResolver}from'@hookform/resolvers/nope';importNopefrom'nope-validator';constschema=Nope.object().shape({name: Nope.string().required(),age: Nope.number().required(),});constApp=()=>{const{ register, handleSubmit }=useForm({resolver: nopeResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('name')}/><inputtype="number"{...register('age')}/><inputtype="submit"/></form>);};TypeScript-first schema validation with static type inference
import{useForm}from'react-hook-form';import{computedTypesResolver}from'@hookform/resolvers/computed-types';importSchema,{number,string}from'computed-types';constschema=Schema({username: string.min(1).error('username field is required'),age: number,});constApp=()=>{const{
register,
handleSubmit,formState: { errors },}=useForm({resolver: computedTypesResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('name')}/>{errors.name?.message&&<p>{errors.name?.message}</p>}<inputtype="number"{...register('age',{valueAsNumber: true})}/>{errors.age?.message&&<p>{errors.age?.message}</p>}<inputtype="submit"/></form>);};Static and runtime type assertion library with no dependencies
import{useForm}from'react-hook-form';import{typanionResolver}from'@hookform/resolvers/typanion';import*astfrom'typanion';constisUser=t.isObject({username: t.applyCascade(t.isString(),[t.hasMinLength(1)]),age: t.applyCascade(t.isNumber(),[t.isInteger(),t.isInInclusiveRange(1,100),]),});constApp=()=>{const{
register,
handleSubmit,formState: { errors },}=useForm({resolver: typanionResolver(isUser),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('name')}/>{errors.name?.message&&<p>{errors.name?.message}</p>}<inputtype="number"{...register('age')}/>{errors.age?.message&&<p>{errors.age?.message}</p>}<inputtype="submit"/></form>);};The fastest JSON validator for Node.js and browser
import{useForm}from'react-hook-form';import{ajvResolver}from'@hookform/resolvers/ajv';// must use `minLength: 1` to implement required fieldconstschema={type: 'object',properties: {username: {type: 'string',minLength: 1,errorMessage: {minLength: 'username field is required'},},password: {type: 'string',minLength: 1,errorMessage: {minLength: 'password field is required'},},},required: ['username','password'],additionalProperties: false,};constApp=()=>{const{
register,
handleSubmit,formState: { errors },}=useForm({resolver: ajvResolver(schema),});return(<formonSubmit={handleSubmit((data)=>console.log(data))}><input{...register('username')}/>{errors.username&&<span>{errors.username.message}</span>}<input{...register('password')}/>{errors.password&&<span>{errors.password.message}</span>}<buttontype="submit">submit</button></form>);};JSON Schema Type Builder with Static Type Resolution for TypeScript
import{useForm}from'react-hook-form';import{typeboxResolver}from'@hookform/resolvers/typebox';import{Type}from'@sinclair/typebox';constschema=Type.Object({username: Type.String({minLength: 1}),password: Type.String({minLength: 1}),});constApp=()=>{const{ register, handleSubmit }=useForm({resolver: typeboxResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('username')}/><inputtype="password"{...register('password')}/><inputtype="submit"/></form>);};A high-performance JIT of TypeBox, read more
import{useForm}from'react-hook-form';import{typeboxResolver}from'@hookform/resolvers/typebox';import{Type}from'@sinclair/typebox';import{TypeCompiler}from'@sinclair/typebox/compiler';constschema=Type.Object({username: Type.String({minLength: 1}),password: Type.String({minLength: 1}),});consttypecheck=TypeCompiler.Compile(schema);constApp=()=>{const{ register, handleSubmit }=useForm({resolver: typeboxResolver(typecheck),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('username')}/><inputtype="password"{...register('password')}/><inputtype="submit"/></form>);};typeboxResolver validates using @sinclair/typebox's own Value.Errors/Value.Check, so any schema
Kind not built into TypeBox (custom types, or ones defined by a third-party library such as ElysiaJS)
must be registered with TypeBox's own TypeRegistry (and FormatRegistry for string formats) before
it's used — this is a TypeBox-level extension point, not something @hookform/resolvers wraps or needs
to expose separately:
import{TypeRegistry}from'@sinclair/typebox';if(!TypeRegistry.Has('Files')){TypeRegistry.Set('Files',(schema,value)=>Array.isArray(value));}Make sure this registration runs against the same @sinclair/typebox module instance the schema was
built with — if a library vendors its own copy of @sinclair/typebox instead of relying on the shared
peer dependency, its registrations won't be visible here, and you'll see a runtime "Unknown type" error.
TypeScript's 1:1 validator, optimized from editor to runtime
import{useForm}from'react-hook-form';import{arktypeResolver}from'@hookform/resolvers/arktype';import{type}from'arktype';constschema=type({username: 'string>1',password: 'string>1',});constApp=()=>{const{ register, handleSubmit }=useForm({resolver: arktypeResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('username')}/><inputtype="password"{...register('password')}/><inputtype="submit"/></form>);};The modular and type safe schema library for validating structural data
import{useForm}from'react-hook-form';import{valibotResolver}from'@hookform/resolvers/valibot';import*asvfrom'valibot';constschema=v.object({username: v.pipe(v.string('username is required'),v.minLength(3,'Needs to be at least 3 characters'),v.endsWith('cool','Needs to end with `cool`'),),password: v.string('password is required'),});constApp=()=>{const{ register, handleSubmit }=useForm({resolver: valibotResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('username')}/><inputtype="password"{...register('password')}/><inputtype="submit"/></form>);};
⚠️ If your schema usesv.transform(...)(orv.pipe(..., v.transform(...))) on a field, that field's parsed output type can differ from what the user actually types (its input type) — e.g. a string input transformed into a number. Passing a single generic touseForm<T>pins both the submitted values and thehandleSubmitpayload to that one type, which conflicts withvalibotResolver, since it infers input and output separately. Either omit the generic and let it infer fromresolver, or specify all three explicitly:constschema=v.object({name: v.string(),number: v.pipe(v.string(),v.transform(Number)),});useForm<v.InferInput<typeofschema>,unknown,v.InferOutput<typeofschema>>({resolver: valibotResolver(schema),});
Universal adapter for schema validation, compatible with any validation library
import{useForm}from'react-hook-form';import{typeschemaResolver}from'@hookform/resolvers/typeschema';import{z}from'zod';// Use your favorite validation libraryconstschema=z.object({username: z.string().min(1,{message: 'Required'}),password: z.number().min(1,{message: 'Required'}),});constApp=()=>{const{ register, handleSubmit }=useForm({resolver: typeschemaResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('username')}/><inputtype="password"{...register('password')}/><inputtype="submit"/></form>);};A powerful TypeScript framework that provides a fully-fledged functional effect system with a rich standard library.
importReactfrom'react';import{useForm}from'react-hook-form';import{effectTsResolver}from'@hookform/resolvers/effect-ts';import{Schema}from'effect';constschema=Schema.Struct({username: Schema.String.pipe(Schema.nonEmptyString({message: ()=>'username required'}),),password: Schema.String.pipe(Schema.nonEmptyString({message: ()=>'password required'}),),});typeFormData=typeofschema.Type;interfaceProps{onSubmit: (data: FormData)=>void;}functionTestComponent({ onSubmit }: Props){const{
register,
handleSubmit,formState: { errors },// provide generic if TS has issues inferring types}=useForm<FormData>({resolver: effectTsResolver(schema),});return(<formonSubmit={handleSubmit(onSubmit)}><input{...register('username')}/>{errors.username&&<spanrole="alert">{errors.username.message}</span>}<input{...register('password')}/>{errors.password&&<spanrole="alert">{errors.password.message}</span>}<buttontype="submit">submit</button></form>);}VineJS is a form data validation library for Node.js
import{useForm}from'react-hook-form';import{vineResolver}from'@hookform/resolvers/vine';importvinefrom'@vinejs/vine';constschema=vine.create({username: vine.string().minLength(1),password: vine.string().minLength(1),});constApp=()=>{const{
register,
handleSubmit,formState: { errors },}=useForm({resolver: vineResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('username')}/>{errors.username&&<spanrole="alert">{errors.username.message}</span>}<input{...register('password')}/>{errors.password&&<spanrole="alert">{errors.password.message}</span>}<buttontype="submit">submit</button></form>);};A TypeScript-first library for building strongly-typed validation rules
import{useForm}from'react-hook-form';import{fluentValidationResolver}from'@hookform/resolvers/fluentvalidation-ts';import{Validator}from'fluentvalidation-ts';classFormDataValidatorextendsValidator<FormData>{constructor(){super();this.ruleFor('username').notEmpty().withMessage('username is a required field');this.ruleFor('password').notEmpty().withMessage('password is a required field');}}constApp=()=>{const{
register,
handleSubmit,formState: { errors },}=useForm({resolver: fluentValidationResolver(newFormDataValidator()),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('username')}/>{errors.username&&<spanrole="alert">{errors.username.message}</span>}<input{...register('password')}/>{errors.password&&<spanrole="alert">{errors.password.message}</span>}<buttontype="submit">submit</button></form>);};A standard interface for TypeScript schema validation libraries
Example zod
import{useForm}from'react-hook-form';import{standardSchemaResolver}from'@hookform/resolvers/standard-schema';import{z}from'zod';constschema=z.object({name: z.string().min(1,{message: 'Required'}),age: z.number().min(10),});constApp=()=>{const{
register,
handleSubmit,formState: { errors },}=useForm({resolver: standardSchemaResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('name')}/>{errors.name?.message&&<p>{errors.name?.message}</p>}<inputtype="number"{...register('age',{valueAsNumber: true})}/>{errors.age?.message&&<p>{errors.age?.message}</p>}<inputtype="submit"/></form>);};Example arkType
import{useForm}from'react-hook-form';import{standardSchemaResolver}from'@hookform/resolvers/standard-schema';import{type}from'arktype';constschema=type({username: 'string>1',password: 'string>1',});constApp=()=>{const{ register, handleSubmit }=useForm({resolver: standardSchemaResolver(schema),});return(<formonSubmit={handleSubmit((d)=>console.log(d))}><input{...register('username')}/><inputtype="password"{...register('password')}/><inputtype="submit"/></form>);};A JSON Schema-based validator for JavaScript and TypeScript
import{useForm}from'react-hook-form';import{ataResolver}from'@hookform/resolvers/ata-validator';constschema={type: 'object',properties: {username: {type: 'string',minLength: 1,},password: {type: 'string',minLength: 1,},},required: ['username','password'],};constApp=()=>{const{
register,
handleSubmit,formState: { errors },}=useForm({resolver: ataResolver(schema),});return(<formonSubmit={handleSubmit((data)=>console.log(data))}><input{...register('username')}/>{errors.username&&<span>{errors.username.message}</span>}<input{...register('password')}/>{errors.password&&<span>{errors.password.message}</span>}<buttontype="submit">submit</button></form>);};Thanks go to all our backers! [Become a backer].
Thanks go to these wonderful people! [Become a contributor].
