@conform-to/zod
Version:
Conform helpers for integrating with Zod
495 lines (483 loc) • 18.4 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var _rollupPluginBabelHelpers = require('../_virtual/_rollupPluginBabelHelpers.js');
var v4 = require('zod/v4');
var _excluded = ["_zod"];
function coerceFile(file) {
if (typeof File !== 'undefined' && file instanceof File && file.name === '' && file.size === 0) {
return undefined;
}
return file;
}
function defaultConvertNumber(text) {
return text.trim() === '' ? NaN : Number(text);
}
function defaultConvertBoolean(text) {
if (text === 'on') {
return true;
}
throw new Error('Not a boolean');
}
function defaultConvertDate(text) {
var date = new Date(text);
if (isNaN(date.getTime())) {
throw new Error('Invalid date');
}
return date;
}
function defaultConvertBigInt(text) {
if (text.trim() === '') {
throw new Error('Empty bigint');
}
return BigInt(text);
}
function coerceString(value, fn) {
if (typeof value !== 'string') {
return value;
}
return fn(value);
}
/**
* strip → undefined stops, else convert, catch → original string
*/
function createValidationCoercion(strip, convert) {
return value => coerceString(value, text => {
var stripped = strip(text);
if (stripped === undefined) {
return undefined;
}
try {
return convert(stripped);
} catch (_unused) {
return stripped;
}
});
}
/**
* On error returns the sentinel (e.g. NaN, false) so typeCheckTransform accepts it.
*/
function createStructuralCoercion(convert, emptySentinel) {
return value => coerceString(value, text => {
try {
return convert(text);
} catch (_unused2) {
return emptySentinel;
}
});
}
function selectCoercion(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;
}
}
/**
* Constrained types (enum, literal) should still be validated in structural
* mode because their values come from constrained UI (select, radio, hidden
* input) and an invalid value indicates a developer error.
*/
function isConstrainedType(type) {
var def = type._zod.def;
return def.type === 'enum' || def.type === 'literal';
}
/**
* Accepts sentinel values like NaN and Invalid Date since
* `typeof NaN === 'number'` and `Invalid Date instanceof Date`.
*/
function getTypeCheckTarget(type) {
switch (type._zod.def.type) {
case 'string':
case 'number':
case 'boolean':
case 'date':
case 'bigint':
return typeCheckTransform(type._zod.def.type);
default:
return type;
}
}
function typeCheckTransform(defType) {
var check = defType === 'date' ? value => value instanceof Date : value => typeof value === defType;
return v4.transform((value, ctx) => {
if (!check(value)) {
ctx.issues.push({
input: value,
code: 'invalid_type',
expected: defType,
received: typeof value,
message: "Invalid input: expected ".concat(defType, ", received ").concat(typeof value)
});
}
return value;
});
}
function pipeWithOptionality(coercion, target) {
var schema = v4.pipe(v4.transform(coercion), target);
if (target._zod.optin) {
schema._zod.optin = target._zod.optin;
}
if (target._zod.optout) {
schema._zod.optout = target._zod.optout;
}
return schema;
}
function materializesMissingValue(type) {
var def = type._zod.def;
if (def.type === 'optional' || def.type === 'nonoptional' || def.type === 'default' || def.type === 'prefault' || def.type === 'catch' || def.type === 'nullable') {
return materializesMissingValue(def.innerType);
}
if (def.type === 'pipe') {
var pipeDef = def;
if (pipeDef.in._zod.def.type === 'transform') {
return materializesMissingValue(pipeDef.out);
}
return materializesMissingValue(pipeDef.in);
}
return def.type === 'array' || def.type === 'object';
}
function withOptionalInput(type) {
var schema = Object.create(Object.getPrototypeOf(type));
var _Object$getOwnPropert = Object.getOwnPropertyDescriptors(type),
descriptors = _rollupPluginBabelHelpers.objectWithoutProperties(_Object$getOwnPropert, _excluded);
Object.defineProperties(schema, descriptors);
Object.defineProperty(schema, '_zod', {
value: Object.create(type._zod, {
optin: {
value: 'optional'
}
})
});
return schema;
}
function coerceObjectShapeEntry(type, options) {
var schema = coerceType(type, options);
if (type._zod.optin !== 'optional' && materializesMissingValue(type)) {
return withOptionalInput(schema);
}
return schema;
}
/**
* Reconstruct the provided schema with additional preprocessing steps
* that strip empty values and coerce strings to the correct type.
*/
function coerceType(type, options) {
var _;
var result = options.resolved.get(type);
if (result) {
// Prevent infinite recursion from z.lazy()
return result;
}
// Detect re-entrant calls caused by getter-based recursive schemas
// (Zod v4's recommended pattern for recursive types).
// Return a lazy wrapper to break the recursion; the inner call is
// deferred to parse time, by which point the schema will be cached.
if (options.resolving.has(type)) {
return v4.lazy(() => coerceType(type, options));
}
options.resolving.add(type);
var schema = type;
var zod = type._zod;
var def = zod.def;
var constr = zod.constr;
var coercion = options.coerce(type);
var target = options.skipValidation && !isConstrainedType(type) ? getTypeCheckTarget(type) : type;
if (coercion) {
schema = pipeWithOptionality(coercion, target);
} else if (target !== type) {
schema = target;
} else if (def.type === 'array') {
var arrayDef = options.skipValidation ? _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, {
checks: undefined
}) : def;
schema = pipeWithOptionality(value => {
if (Array.isArray(value)) {
return value;
}
if (typeof value === 'undefined' || options.stripEmptyValue && typeof options.stripEmptyValue(value) === 'undefined') {
return [];
}
return [value];
}, new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, arrayDef), {}, {
element: coerceType(def.element, options)
})));
} else if (def.type === 'object') {
var objectDef = options.skipValidation ? _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, {
catchall: undefined
}) : def;
schema = pipeWithOptionality(value => {
if (typeof value === 'undefined') {
return {};
}
return value;
}, new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, objectDef), {}, {
shape: Object.fromEntries(Object.entries(def.shape).map(_ref => {
var [key, def] = _ref;
return [key, coerceObjectShapeEntry(def, options)];
}))
})));
} else if (def.type === 'optional' || def.type === 'nonoptional') {
var innerType = coerceType(def.innerType, options);
var wrapped = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, {
innerType
}));
schema = options.stripEmptyValue ? pipeWithOptionality(options.stripEmptyValue, wrapped) : wrapped;
} else if (def.type === 'default' || def.type === 'prefault') {
if (options.skipDefaults) {
schema = v4.optional(coerceType(def.innerType, options));
} else {
var defaultValue = def.defaultValue;
var _innerType = defaultValue !== '' // Don't strip the empty string that IS the default
? coerceType(def.innerType, options) : def.innerType;
var _wrapped = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, {
innerType: _innerType
}));
schema = options.stripEmptyValue ? pipeWithOptionality(options.stripEmptyValue, _wrapped) : _wrapped;
}
} else if (def.type === 'catch') {
schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, {
innerType: coerceType(def.innerType, options)
}));
} else if (def.type === 'intersection') {
schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, {
left: coerceType(def.left, options),
right: coerceType(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 = v4.pipe(v4.transform(value => {
if (typeof value === 'undefined') {
return {};
}
return value;
}), new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, {
options: def.options.map(item => {
var objectDef = item._zod.def;
var setOriginalPropValues = object => {
// 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;
};
if (objectDef.type !== 'object') {
return setOriginalPropValues(coerceType(item, options));
}
var innerDef = options.skipValidation ? _rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, objectDef), {}, {
catchall: undefined
}) : objectDef;
var object = new item._zod.constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, innerDef), {}, {
shape: Object.fromEntries(Object.entries(objectDef.shape).map(_ref2 => {
var [key, def] = _ref2;
return [key, coerceObjectShapeEntry(def, options)];
}))
}));
return setOriginalPropValues(object);
})
})));
} else if (def.type === 'union') {
schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, {
options: def.options.map(item => coerceType(item, options))
}));
} else if (def.type === 'tuple') {
schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, {
items: def.items.map(item => coerceType(item, options))
}));
} else if (def.type === 'nullable') {
schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, {
innerType: coerceType(def.innerType, options)
}));
} else if (def.type === 'pipe') {
if (options.skipTransforms) {
schema = coerceType(def.in, options);
} else {
schema = new constr(_rollupPluginBabelHelpers.objectSpread2(_rollupPluginBabelHelpers.objectSpread2({}, def), {}, {
in: coerceType(def.in, options),
out: coerceType(def.out, options)
}));
}
} else if (def.type === 'lazy') {
var inner = def.getter();
schema = v4.lazy(() => coerceType(inner, options));
}
options.resolving.delete(type);
if (type !== schema) {
options.resolved.set(type, schema);
}
return schema;
}
/**
* Creates configured coercion functions for form value parsing.
*
* **Example:**
*
* ```tsx
* import { configureCoercion } from '@conform-to/zod/v4/future';
* import { z } from 'zod';
*
* const { coerceFormValue, coerceStructure } = configureCoercion({
* // Trim whitespace and treat whitespace-only as empty
* stripEmptyString: (value) => {
* const trimmed = value.trim();
* return trimmed === '' ? undefined : trimmed;
* },
* type: {
* // Custom number parsing: strip commas
* number: (text) => Number(text.replace(/,/g, '')),
* },
* });
*
* const schema = z.object({ age: z.number(), name: z.string() });
* const validationSchema = coerceFormValue(schema);
* const structuralSchema = coerceStructure(schema);
* ```
*/
function configureCoercion(config) {
var _config$stripEmptyStr, _config$type$number, _config$type, _config$type$boolean, _config$type2, _config$type$date, _config$type3;
var stripEmptyString = (_config$stripEmptyStr = config === null || config === void 0 ? void 0 : config.stripEmptyString) !== null && _config$stripEmptyStr !== void 0 ? _config$stripEmptyStr : value => value === '' ? undefined : value;
var convertNumber = (_config$type$number = config === null || config === void 0 || (_config$type = config.type) === null || _config$type === void 0 ? void 0 : _config$type.number) !== null && _config$type$number !== void 0 ? _config$type$number : defaultConvertNumber;
var convertBoolean = (_config$type$boolean = config === null || config === void 0 || (_config$type2 = config.type) === null || _config$type2 === void 0 ? void 0 : _config$type2.boolean) !== null && _config$type$boolean !== void 0 ? _config$type$boolean : defaultConvertBoolean;
var convertDate = (_config$type$date = config === null || config === void 0 || (_config$type3 = config.type) === null || _config$type3 === void 0 ? void 0 : _config$type3.date) !== null && _config$type$date !== void 0 ? _config$type$date : defaultConvertDate;
var validationMap = {
string: value => coerceString(value, stripEmptyString),
file: coerceFile,
number: createValidationCoercion(stripEmptyString, convertNumber),
boolean: createValidationCoercion(stripEmptyString, convertBoolean),
date: createValidationCoercion(stripEmptyString, convertDate),
bigint: createValidationCoercion(stripEmptyString, defaultConvertBigInt)
};
var structuralMap = {
number: createStructuralCoercion(convertNumber, NaN),
boolean: createStructuralCoercion(convertBoolean, false),
date: createStructuralCoercion(convertDate, new Date('')),
bigint: createStructuralCoercion(defaultConvertBigInt, 0n)
};
var coerce = (type, coercionMap) => {
var _selectCoercion;
if (config !== null && config !== void 0 && config.customize) {
var customFn = config.customize(type);
if (customFn !== null) {
return customFn;
}
}
return (_selectCoercion = selectCoercion(type, coercionMap)) !== null && _selectCoercion !== void 0 ? _selectCoercion : null;
};
var validationCache = new WeakMap();
var structureCache = new WeakMap();
return {
/**
* Enhances a schema to coerce form values and strip empty values before validation.
* This configured helper uses the options passed to `configureCoercion`.
*
* Results are cached per schema, so this can be called inline.
*
* **Example:**
*
* ```tsx
* const schema = coerceFormValue(z.object({
* age: z.number().optional(),
* subscribe: z.boolean(),
* }));
*
* schema.parse({ age: '', subscribe: 'on' });
* // { age: undefined, subscribe: true }
* ```
*/
coerceFormValue(type) {
var result = validationCache.get(type);
if (!result) {
result = coerceType(type, {
resolved: new Map(),
resolving: new Set(),
stripEmptyValue(value) {
return coerceFile(coerceString(value, stripEmptyString));
},
coerce(type) {
return coerce(type, validationMap);
}
});
validationCache.set(type, result);
}
return result;
},
/**
* Enhances a schema to coerce form values without running validation.
* This configured helper is useful for reading current form values as typed data.
*
* It skips validation, defaults, transforms, and refinements, and does not strip
* empty strings to `undefined`.
*
* For number, boolean, date, and bigint schemas, empty strings and other failed
* string coercions still become fallback values:
*
* - `z.number()` -> `NaN`
* - `z.boolean()` -> `false`
* - `z.date()` -> `Invalid Date`
* - `z.bigint()` -> `0n`
*
* Results are cached per schema, so this can be called inline.
*
* **Example:**
*
* ```tsx
* const schema = coerceStructure(z.object({
* age: z.number().min(10),
* }));
*
* schema.parse({ age: '3' });
* // { age: 3 }
* ```
*/
coerceStructure(type) {
var result = structureCache.get(type);
if (!result) {
result = coerceType(type, {
resolved: new Map(),
resolving: new Set(),
coerce(type) {
return coerce(type, structuralMap);
},
skipValidation: true,
skipDefaults: true,
skipTransforms: true
});
structureCache.set(type, result);
}
return result;
}
};
}
var {
coerceFormValue,
coerceStructure
} = configureCoercion();
exports.coerceFormValue = coerceFormValue;
exports.coerceStructure = coerceStructure;
exports.configureCoercion = configureCoercion;