@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
142 lines (126 loc) • 4.15 kB
text/typescript
import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';
import {
FieldError,
FieldErrors,
FieldValues,
Resolver,
ResolverError,
ResolverSuccess,
appendErrors,
} from 'react-hook-form';
import { ZodError, z } from 'zod';
const isZodError = (error: any): error is ZodError =>
Array.isArray(error?.errors);
function parseErrorSchema(
zodErrors: z.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;
}
export function zodResolver<Input extends FieldValues, Context, Output>(
schema: z.ZodSchema<Output, any, Input>,
schemaOptions?: Partial<z.ParseParams>,
resolverOptions?: {
mode?: 'async' | 'sync';
raw?: false;
},
): Resolver<Input, Context, Output>;
export function zodResolver<Input extends FieldValues, Context, Output>(
schema: z.ZodSchema<Output, any, Input>,
schemaOptions: Partial<z.ParseParams> | undefined,
resolverOptions: {
mode?: 'async' | 'sync';
raw: true;
},
): Resolver<Input, Context, Input>;
/**
* Creates a resolver function for react-hook-form that validates form data using a Zod schema
* @param {z.ZodSchema<Input>} schema - The Zod schema used to validate the form data
* @param {Partial<z.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<z.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 = z.object({
* name: z.string().min(2),
* age: z.number().min(18)
* });
*
* useForm({
* resolver: zodResolver(schema)
* });
*/
export function zodResolver<Input extends FieldValues, Context, Output>(
schema: z.ZodSchema<Output, any, Input>,
schemaOptions?: Partial<z.ParseParams>,
resolverOptions: {
mode?: 'async' | 'sync';
raw?: boolean;
} = {},
): Resolver<Input, Context, Output | Input> {
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 (isZodError(error)) {
return {
values: {},
errors: toNestErrors(
parseErrorSchema(
error.errors,
!options.shouldUseNativeValidation &&
options.criteriaMode === 'all',
),
options,
),
} satisfies ResolverError<Input>;
}
throw error;
}
};
}