UNPKG

@conform-to/zod

Version:

Conform helpers for integrating with Zod

319 lines (308 loc) 11.1 kB
import { objectSpread2 as _objectSpread2 } from '../_virtual/_rollupPluginBabelHelpers.mjs'; import { pipe, transform, lazy } from 'zod/v4-mini'; /** * Helpers for coercing string value * Modify the value only if it's a string, otherwise return the value as-is */ function stripEmptyString(value) { if (typeof value !== 'string') { return value; } if (value === '') { return undefined; } return value; } /** * Helpers for coercing file * Modify the value only if it's a file, otherwise return the value as-is */ function stripEmptyFile(file) { if (typeof File !== 'undefined' && file instanceof File && file.name === '' && file.size === 0) { return undefined; } return file; } function coerceNumber(value) { if (typeof value !== 'string') { return value; } return value.trim() === '' ? value : Number(value); } function coerceBoolean(value) { if (typeof value !== 'string') { return value; } return value === 'on' ? true : value; } function coerceDate(value) { if (typeof value !== 'string') { return value; } var date = new Date(value); // z.date() does not expose a quick way to set invalid_date error // This gets around it by returning the original string if it's invalid // See https://github.com/colinhacks/zod/issues/1526 if (isNaN(date.getTime())) { return value; } return date; } function coerceBigInt(value) { if (typeof value !== 'string') { return value; } if (value.trim() === '') { return value; } try { return BigInt(value); } catch (_unused) { return value; } } function compose(a, b) { var c = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : i => i; return value => c(b(a(value))); } function selectDefaultCoercion(type, defaultCoercion) { var def = type._zod.def; if (def.type === 'string' || def.type === 'enum' // || def.type === 'nativeEnum' ) { return defaultCoercion.string; } else if (def.type === 'literal') { if (!('values' in def)) { return defaultCoercion.string; } var literalValue = [...def.values][0]; if (typeof literalValue === 'number') { return defaultCoercion.number; } if (typeof literalValue === 'boolean') { return defaultCoercion.boolean; } if (typeof literalValue === 'bigint') { return defaultCoercion.bigint; } return defaultCoercion.string; } else if (def.type === 'file') { return defaultCoercion.file; } else if (def.type === 'number') { return defaultCoercion.number; } else if (def.type === 'boolean') { return defaultCoercion.boolean; } else if (def.type === 'date') { return defaultCoercion.date; } else if (def.type === 'bigint') { return defaultCoercion.bigint; } return null; } /** * Reconstruct the provided schema with additional preprocessing steps * This strips empty values to undefined and coerces string to the correct type */ function enableTypeCoercion(type, options) { var _; var result = options.cache.get(type); // Return the cached schema if it's already processed // This is to prevent infinite recursion caused by z.lazy() if (result) { return result; } var schema = type; var zod = type._zod; var def = zod.def; var constr = zod.constr; var coercion = options.coerce(type); if (coercion) { schema = pipe(transform(coercion), type); } else if (def.type === 'array') { schema = pipe(transform(value => { // No preprocess needed if the value is already an array if (Array.isArray(value)) { return value; } if (typeof value === 'undefined' || typeof options.stripEmptyValue(value) === 'undefined') { return []; } // Wrap it in an array otherwise return [value]; }), new constr(_objectSpread2(_objectSpread2({}, def), {}, { element: enableTypeCoercion(def.element, options) }))); } else if (def.type === 'object') { schema = pipe(transform(value => { if (typeof value === 'undefined') { // Defaults it to an empty object return {}; } return value; }), new constr(_objectSpread2(_objectSpread2({}, def), {}, { shape: Object.fromEntries(Object.entries(def.shape).map(_ref => { var [key, def] = _ref; return [key, enableTypeCoercion(def, options)]; })) }))); } else if (def.type === 'optional') { schema = pipe(transform(options.stripEmptyValue), new constr(_objectSpread2(_objectSpread2({}, def), {}, { innerType: enableTypeCoercion(def.innerType, options) }))); } else if (def.type === 'default') { schema = pipe(transform(options.stripEmptyValue), new constr(_objectSpread2(_objectSpread2({}, def), {}, { innerType: enableTypeCoercion(def.innerType, options) }))); } else if (def.type === 'catch') { schema = new constr(_objectSpread2(_objectSpread2({}, def), {}, { innerType: enableTypeCoercion(def.innerType, options) })); } else if (def.type === 'intersection') { schema = new constr(_objectSpread2(_objectSpread2({}, def), {}, { left: enableTypeCoercion(def.left, options), right: enableTypeCoercion(def.right, options) })); // The `type` of `discriminatedUnion` is defined as 'union', so it can be determined from the class name used. } else if (def.type === 'union' && (_ = [...zod.traits][0]) !== null && _ !== void 0 && _.includes('DiscriminatedUnion')) { schema = pipe(transform(value => { if (typeof value === 'undefined') { // Defaults it to an empty object return {}; } return value; }), new constr(_objectSpread2(_objectSpread2({}, def), {}, { options: def.options.map(item => { var objectDef = item._zod.def; var object = new item._zod.constr(_objectSpread2(_objectSpread2({}, objectDef), {}, { shape: Object.fromEntries(Object.entries(objectDef.shape).map(_ref2 => { var [key, def] = _ref2; return [key, enableTypeCoercion(def, options)]; })) })); // The discriminate key is obtained from the defined Object. // If you regenerate the Object schema, the `propValues` property disappears. Therefore, set the one obtained from the original Object. // https://github.com/colinhacks/zod/blob/22ab436bc214d86d740e78f33ae6834d28ddc152/packages/zod/src/v4/core/schemas.ts#L1949-L1963 object._zod.propValues = item._zod.propValues; // @ts-expect-error: The `disc` property was used up to version 3.25.34, but was changed to the `propValues` property from version 3.25.35 onwards. object._zod.disc = item._zod.disc; return object; }) }))); } else if (def.type === 'union') { schema = new constr(_objectSpread2(_objectSpread2({}, def), {}, { options: def.options.map(item => enableTypeCoercion(item, options)) })); } else if (def.type === 'tuple') { schema = new constr(_objectSpread2(_objectSpread2({}, def), {}, { items: def.items.map(item => enableTypeCoercion(item, options)) })); } else if (def.type === 'nullable') { schema = new constr(_objectSpread2(_objectSpread2({}, def), {}, { innerType: enableTypeCoercion(def.innerType, options) })); } else if (def.type === 'pipe') { schema = new constr(_objectSpread2(_objectSpread2({}, def), {}, { in: enableTypeCoercion(def.in, options), out: enableTypeCoercion(def.out, options) })); } else if (def.type === 'lazy') { var inner = def.getter(); schema = lazy(() => enableTypeCoercion(inner, options)); } if (type !== schema) { options.cache.set(type, schema); } return schema; } /** * A helper that enhance the zod schema to strip empty value and coerce form value to the expected type with option to customize type coercion. * @example * * ```tsx * import { parseWithZod, unstable_coerceFormValue as coerceFormValue } from '@conform-to/zod'; * import { z } from 'zod'; * * // Coerce the form value with default behaviour * const schema = coerceFormValue( * z.object({ * // ... * }) * ); * * // Coerce the form value with default coercion overrided * const schema = coerceFormValue( * z.object({ * ref: z.number() * date: z.date(), * amount: z.number(), * confirm: z.boolean(), * }), * { * // Trim the value for all string-based fields * // e.g. `z.string()`, `z.number()` or `z.boolean()` * string: (value) => { * if (typeof value !== 'string') { * return value; * } * * const result = value.trim(); * * // Treat it as `undefined` if the value is empty * if (result === '') { * return undefined; * } * * return result; * }, * * // Override the default coercion with `z.number()` * number: (value) => { * // Pass the value as is if it's not a string * if (typeof value !== 'string') { * return value; * } * * // Trim and remove commas before casting it to number * return Number(value.trim().replace(/,/g, '')); * }, * * // Disable coercion for `z.boolean()` * boolean: false, * }, * ); * ``` */ function coerceFormValue(type, options) { var getCoercion = (type, fallbackCoercion) => { var _options$defaultCoerc; var providedCoercion = options === null || options === void 0 || (_options$defaultCoerc = options.defaultCoercion) === null || _options$defaultCoerc === void 0 ? void 0 : _options$defaultCoerc[type]; if (typeof providedCoercion === 'function') { return providedCoercion; } // If the user explicitly disabled the coercion or no fallback coercion, return a noop function if (providedCoercion === false || !fallbackCoercion) { return value => value; } return fallbackCoercion; }; var defaultCoercion = { string: compose(stripEmptyString, getCoercion('string')), file: compose(stripEmptyFile, getCoercion('file')), number: compose(stripEmptyString, getCoercion('string'), getCoercion('number', coerceNumber)), boolean: compose(stripEmptyString, getCoercion('string'), getCoercion('boolean', coerceBoolean)), date: compose(stripEmptyString, getCoercion('string'), getCoercion('date', coerceDate)), bigint: compose(stripEmptyString, getCoercion('string'), getCoercion('bigint', coerceBigInt)) }; return enableTypeCoercion(type, { cache: new Map(), stripEmptyValue: compose(defaultCoercion.string, defaultCoercion.file), coerce: type => { var _options$customize, _options$customize2; var coercion = (_options$customize = options === null || options === void 0 || (_options$customize2 = options.customize) === null || _options$customize2 === void 0 ? void 0 : _options$customize2.call(options, type)) !== null && _options$customize !== void 0 ? _options$customize : null; if (coercion === null) { coercion = selectDefaultCoercion(type, defaultCoercion); } return coercion; } }); } export { coerceFormValue, enableTypeCoercion };