UNPKG

@hookform/resolvers

Version:

React Hook Form validation resolvers: Yup, Joi, Superstruct, Zod, Vest, Class Validator, io-ts, Nope, computed-types, TypeBox, arktype, Typanion, Effect-TS and VineJS

316 lines (286 loc) 8.91 kB
import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers'; import { FieldError, FieldErrors, FieldValues, Resolver, ResolverError, ResolverSuccess, appendErrors, } from 'react-hook-form'; import * as z3 from 'zod/v3'; import * as z4 from 'zod/v4/core'; const isZod3Error = (error: any): error is z3.ZodError => { return Array.isArray(error?.issues); }; const isZod3Schema = (schema: any): schema is z3.ZodSchema => { return ( '_def' in schema && typeof schema._def === 'object' && 'typeName' in schema._def ); }; const isZod4Error = (error: any): error is z4.$ZodError => { // instanceof is safe in Zod 4 (uses Symbol.hasInstance) return error instanceof z4.$ZodError; }; const isZod4Schema = (schema: any): schema is z4.$ZodType => { return '_zod' in schema && typeof schema._zod === 'object'; }; function parseZod3Issues( zodErrors: z3.ZodIssue[], validateAllFieldCriteria: boolean, ) { const errors: Record<string, FieldError> = {}; for (; zodErrors.length; ) { const error = zodErrors[0]; const { code, message, path } = error; const _path = path.join('.'); if (!errors[_path]) { if ('unionErrors' in error) { const unionError = error.unionErrors[0].errors[0]; errors[_path] = { message: unionError.message, type: unionError.code, }; } else { errors[_path] = { message, type: code }; } } if ('unionErrors' in error) { error.unionErrors.forEach((unionError) => unionError.errors.forEach((e) => zodErrors.push(e)), ); } if (validateAllFieldCriteria) { const types = errors[_path].types; const messages = types && types[error.code]; errors[_path] = appendErrors( _path, validateAllFieldCriteria, errors, code, messages ? ([] as string[]).concat(messages as string[], error.message) : error.message, ) as FieldError; } zodErrors.shift(); } return errors; } function parseZod4Issues( zodErrors: z4.$ZodIssue[], validateAllFieldCriteria: boolean, ) { const errors: Record<string, FieldError> = {}; // const _zodErrors = zodErrors as z4.$ZodISsue; // for (; zodErrors.length; ) { const error = zodErrors[0]; const { code, message, path } = error; const _path = path.join('.'); if (!errors[_path]) { if (error.code === 'invalid_union' && error.errors.length > 0) { const unionError = error.errors[0][0]; errors[_path] = { message: unionError.message, type: unionError.code, }; } else { errors[_path] = { message, type: code }; } } if (error.code === 'invalid_union') { error.errors.forEach((unionError) => unionError.forEach((e) => zodErrors.push(e)), ); } if (validateAllFieldCriteria) { const types = errors[_path].types; const messages = types && types[error.code]; errors[_path] = appendErrors( _path, validateAllFieldCriteria, errors, code, messages ? ([] as string[]).concat(messages as string[], error.message) : error.message, ) as FieldError; } zodErrors.shift(); } return errors; } type RawResolverOptions = { mode?: 'async' | 'sync'; raw: true; }; type NonRawResolverOptions = { mode?: 'async' | 'sync'; raw?: false; }; // minimal interfaces to avoid asssignability issues between versions interface Zod3Type<O = unknown, I = unknown> { _output: O; _input: I; _def: { typeName: string; }; } // some type magic to make versions pre-3.25.0 still work type IsUnresolved<T> = PropertyKey extends keyof T ? true : false; type UnresolvedFallback<T, Fallback> = IsUnresolved<typeof z3> extends true ? Fallback : T; type FallbackIssue = { code: string; message: string; path: (string | number)[]; }; type Zod3ParseParams = UnresolvedFallback< z3.ParseParams, // fallback if user is on <3.25.0 { path?: (string | number)[]; errorMap?: ( iss: FallbackIssue, ctx: { defaultError: string; data: any; }, ) => { message: string }; async?: boolean; } >; type Zod4ParseParams = UnresolvedFallback< z4.ParseContext<z4.$ZodIssue>, // fallback if user is on <3.25.0 { readonly error?: ( iss: FallbackIssue, ) => null | undefined | string | { message: string }; readonly reportInput?: boolean; readonly jitless?: boolean; } >; export function zodResolver<Input extends FieldValues, Context, Output>( schema: Zod3Type<Output, Input>, schemaOptions?: Zod3ParseParams, resolverOptions?: NonRawResolverOptions, ): Resolver<Input, Context, Output>; export function zodResolver<Input extends FieldValues, Context, Output>( schema: Zod3Type<Output, Input>, schemaOptions: Zod3ParseParams | undefined, resolverOptions: RawResolverOptions, ): Resolver<Input, Context, Input>; // the Zod 4 overloads need to be generic for complicated reasons export function zodResolver< Input extends FieldValues, Context, Output, T extends z4.$ZodType<Output, Input> = z4.$ZodType<Output, Input>, >( schema: T, schemaOptions?: Zod4ParseParams, // already partial resolverOptions?: NonRawResolverOptions, ): Resolver<z4.input<T>, Context, z4.output<T>>; export function zodResolver< Input extends FieldValues, Context, Output, T extends z4.$ZodType<Output, Input> = z4.$ZodType<Output, Input>, >( schema: z4.$ZodType<Output, Input>, schemaOptions: Zod4ParseParams | undefined, // already partial resolverOptions: RawResolverOptions, ): Resolver<z4.input<T>, Context, z4.output<T>>; /** * Creates a resolver function for react-hook-form that validates form data using a Zod schema * @param {z3.ZodSchema<Input>} schema - The Zod schema used to validate the form data * @param {Partial<z3.ParseParams>} [schemaOptions] - Optional configuration options for Zod parsing * @param {Object} [resolverOptions] - Optional resolver-specific configuration * @param {('async'|'sync')} [resolverOptions.mode='async'] - Validation mode. Use 'sync' for synchronous validation * @param {boolean} [resolverOptions.raw=false] - If true, returns the raw form values instead of the parsed data * @returns {Resolver<z3.output<typeof schema>>} A resolver function compatible with react-hook-form * @throws {Error} Throws if validation fails with a non-Zod error * @example * const schema = z3.object({ * name: z3.string().min(2), * age: z3.number().min(18) * }); * * useForm({ * resolver: zodResolver(schema) * }); */ export function zodResolver<Input extends FieldValues, Context, Output>( schema: object, schemaOptions?: object, resolverOptions: { mode?: 'async' | 'sync'; raw?: boolean; } = {}, ): Resolver<Input, Context, Output | Input> { if (isZod3Schema(schema)) { return async (values: Input, _, options) => { try { const data = await schema[ resolverOptions.mode === 'sync' ? 'parse' : 'parseAsync' ](values, schemaOptions); options.shouldUseNativeValidation && validateFieldsNatively({}, options); return { errors: {} as FieldErrors, values: resolverOptions.raw ? Object.assign({}, values) : data, } satisfies ResolverSuccess<Output | Input>; } catch (error) { if (isZod3Error(error)) { return { values: {}, errors: toNestErrors( parseZod3Issues( error.errors, !options.shouldUseNativeValidation && options.criteriaMode === 'all', ), options, ), } satisfies ResolverError<Input>; } throw error; } }; } if (isZod4Schema(schema)) { return async (values: Input, _, options) => { try { const parseFn = resolverOptions.mode === 'sync' ? z4.parse : z4.parseAsync; const data: any = await parseFn(schema, values, schemaOptions); options.shouldUseNativeValidation && validateFieldsNatively({}, options); return { errors: {} as FieldErrors, values: resolverOptions.raw ? Object.assign({}, values) : data, } satisfies ResolverSuccess<Output | Input>; } catch (error) { if (isZod4Error(error)) { return { values: {}, errors: toNestErrors( parseZod4Issues( error.issues, !options.shouldUseNativeValidation && options.criteriaMode === 'all', ), options, ), } satisfies ResolverError<Input>; } throw error; } }; } throw new Error('Invalid input: not a Zod schema'); }