@conform-to/zod
Version:
Conform helpers for integrating with Zod
304 lines (293 loc) • 11 kB
JavaScript
import { objectSpread2 as _objectSpread2 } from './_virtual/_rollupPluginBabelHelpers.mjs';
import { any, ZodArray, ZodObject, ZodEffects, ZodOptional, ZodCatch, ZodIntersection, ZodUnion, ZodDiscriminatedUnion, ZodBranded, ZodTuple, ZodNullable, ZodPipeline, lazy } from 'zod';
/**
* 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;
}
}
/**
* A file schema is usually defined with `z.instanceof(File)`
* which is implemented with `z.custom()` based on ZodAny with a `superRefine` check
* @see https://github.com/colinhacks/zod/blob/eea05ae3dab628e7a834397414e5145e935e418b/src/types.ts#L5250-L5285
*/
function isFileSchema(schema) {
if (typeof File === 'undefined') {
return false;
}
return schema._def.effect.type === 'refinement' && schema.innerType()._def.typeName === 'ZodAny' && schema.safeParse(new File([], '')).success && !schema.safeParse('').success;
}
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._def;
if (def.typeName === 'ZodString' || def.typeName === 'ZodLiteral' || def.typeName === 'ZodEnum' || def.typeName === 'ZodNativeEnum') {
return defaultCoercion.string;
} else if (def.typeName === 'ZodEffects' && isFileSchema(type)) {
return defaultCoercion.file;
} else if (def.typeName === 'ZodNumber') {
return defaultCoercion.number;
} else if (def.typeName === 'ZodBoolean') {
return defaultCoercion.boolean;
} else if (def.typeName === 'ZodDate') {
return defaultCoercion.date;
} else if (def.typeName === 'ZodBigInt') {
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 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 def = type._def;
var coercion = options.coerce(type);
if (coercion) {
schema = any().transform(coercion).pipe(type);
} else if (def.typeName === 'ZodArray') {
schema = any().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];
}).pipe(new ZodArray(_objectSpread2(_objectSpread2({}, def), {}, {
type: enableTypeCoercion(def.type, options)
})));
} else if (def.typeName === 'ZodObject') {
schema = any().transform(value => {
if (typeof value === 'undefined') {
// Defaults it to an empty object
return {};
}
return value;
}).pipe(new ZodObject(_objectSpread2(_objectSpread2({}, def), {}, {
shape: () => Object.fromEntries(Object.entries(def.shape()).map(_ref => {
var [key, def] = _ref;
return [key,
// @ts-expect-error see message above
enableTypeCoercion(def, options)];
}))
})));
} else if (def.typeName === 'ZodEffects') {
schema = new ZodEffects(_objectSpread2(_objectSpread2({}, def), {}, {
schema: enableTypeCoercion(def.schema, options)
}));
} else if (def.typeName === 'ZodOptional') {
schema = any().transform(options.stripEmptyValue).pipe(new ZodOptional(_objectSpread2(_objectSpread2({}, def), {}, {
innerType: enableTypeCoercion(def.innerType, options)
})));
} else if (def.typeName === 'ZodDefault') {
var defaultValue = def.defaultValue();
schema = any().transform(options.stripEmptyValue)
// Reconstruct `.default()` as `.optional().transform(value => value ?? defaultValue)`
.pipe(enableTypeCoercion(def.innerType, options).optional()).transform(value => value !== null && value !== void 0 ? value : defaultValue);
} else if (def.typeName === 'ZodCatch') {
schema = new ZodCatch(_objectSpread2(_objectSpread2({}, def), {}, {
innerType: enableTypeCoercion(def.innerType, options)
}));
} else if (def.typeName === 'ZodIntersection') {
schema = new ZodIntersection(_objectSpread2(_objectSpread2({}, def), {}, {
left: enableTypeCoercion(def.left, options),
right: enableTypeCoercion(def.right, options)
}));
} else if (def.typeName === 'ZodUnion') {
schema = new ZodUnion(_objectSpread2(_objectSpread2({}, def), {}, {
options: def.options.map(option => enableTypeCoercion(option, options))
}));
} else if (def.typeName === 'ZodDiscriminatedUnion') {
schema = new ZodDiscriminatedUnion(_objectSpread2(_objectSpread2({}, def), {}, {
options: def.options.map(option => enableTypeCoercion(option, options)),
optionsMap: new Map(Array.from(def.optionsMap.entries()).map(_ref2 => {
var [discriminator, option] = _ref2;
return [discriminator, enableTypeCoercion(option, options)];
}))
}));
} else if (def.typeName === 'ZodBranded') {
schema = new ZodBranded(_objectSpread2(_objectSpread2({}, def), {}, {
type: enableTypeCoercion(def.type, options)
}));
} else if (def.typeName === 'ZodTuple') {
schema = new ZodTuple(_objectSpread2(_objectSpread2({}, def), {}, {
items: def.items.map(item => enableTypeCoercion(item, options))
}));
} else if (def.typeName === 'ZodNullable') {
schema = new ZodNullable(_objectSpread2(_objectSpread2({}, def), {}, {
innerType: enableTypeCoercion(def.innerType, options)
}));
} else if (def.typeName === 'ZodPipeline') {
schema = new ZodPipeline(_objectSpread2(_objectSpread2({}, def), {}, {
in: enableTypeCoercion(def.in, options),
out: enableTypeCoercion(def.out, options)
}));
} else if (def.typeName === 'ZodLazy') {
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 };