UNPKG

zod-form-data

Version:

Validation helpers for [zod](https://github.com/colinhacks/zod) specifically for parsing `FormData` or `URLSearchParams`. This is particularly useful when using [Remix](https://github.com/remix-run/remix) and combos well with [remix-validated-form](https:

117 lines (116 loc) 4.56 kB
import { setPath } from "set-get"; import { z, ZodType, } from "zod"; const stripEmpty = z.literal("").transform(() => undefined); const preprocessIfValid = (schema) => (val) => { const result = schema.safeParse(val); if (result.success) return result.data; return val; }; /** * Transforms any empty strings to `undefined` before validating. * This makes it so empty strings will fail required checks, * allowing you to use `optional` for optional fields instead of `nonempty` for required fields. * If you call `zfd.text` with no arguments, it will assume the field is a required string by default. * If you want to customize the schema, you can pass that as an argument. */ export const text = (schema = z.string()) => z.preprocess(preprocessIfValid(stripEmpty), schema); /** * Coerces numerical strings to numbers transforms empty strings to `undefined` before validating. * If you call `zfd.number` with no arguments, * it will assume the field is a required number by default. * If you want to customize the schema, you can pass that as an argument. */ export const numeric = (schema = z.number()) => z.preprocess(preprocessIfValid(z.union([ stripEmpty, z .string() .transform((val) => Number(val)) .refine((val) => !Number.isNaN(val)), ])), schema); /** * Turns the value from a checkbox field into a boolean, * but does not require the checkbox to be checked. * For checkboxes with a `value` attribute, you can pass that as the `trueValue` option. * * @example * ```ts * const schema = zfd.formData({ * defaultCheckbox: zfd.checkbox(), * checkboxWithValue: zfd.checkbox({ trueValue: "true" }), * mustBeTrue: zfd * .checkbox() * .refine((val) => val, "Please check this box"), * }); * }); * ``` */ export const checkbox = ({ trueValue = "on" } = {}) => z.union([ z.literal(trueValue).transform(() => true), z.literal(undefined).transform(() => false), ]); export const file = (schema = z.instanceof(File)) => z.preprocess((val) => { //Empty File object on no user input, so convert to undefined return val instanceof File && val.size === 0 ? undefined : val; }, schema); /** * Preprocesses a field where you expect multiple values could be present for the same field name * and transforms the value of that field to always be an array. * If you don't provide a schema, it will assume the field is an array of zfd.text fields * and will not require any values to be present. */ export const repeatable = (schema = z.array(text())) => { return z.preprocess((val) => { if (Array.isArray(val)) return val; if (val === undefined) return []; return [val]; }, schema); }; /** * A convenience wrapper for repeatable. * Instead of passing the schema for an entire array, you pass in the schema for the item type. */ export const repeatableOfType = (schema) => repeatable(z.array(schema)); const entries = z.array(z.tuple([z.string(), z.any()])); const safeParseJson = (jsonString) => { try { return JSON.parse(jsonString); } catch { return jsonString; } }; export const json = (schema) => z.preprocess(preprocessIfValid(z.union([stripEmpty, z.string().transform((val) => safeParseJson(val))])), schema); const processFormData = preprocessIfValid( // We're avoiding using `instanceof` here because different environments // won't necessarily have `FormData` or `URLSearchParams` z .any() .refine((val) => Symbol.iterator in val) .transform((val) => [...val]) .refine((val) => entries.safeParse(val).success) .transform((data) => { const map = new Map(); for (const [key, value] of data) { if (map.has(key)) { map.get(key).push(value); } else { map.set(key, [value]); } } return [...map.entries()].reduce((acc, [key, value]) => { return setPath(acc, key, value.length === 1 ? value[0] : value); }, {}); })); export const preprocessFormData = processFormData; /** * This helper takes the place of the `z.object` at the root of your schema. * It wraps your schema in a `z.preprocess` that extracts all the data out of a `FormData` * and transforms it into a regular object. * If the `FormData` contains multiple entries with the same field name, * it will automatically turn that field into an array. */ export const formData = (shapeOrSchema) => z.preprocess(processFormData, shapeOrSchema instanceof ZodType ? shapeOrSchema : z.object(shapeOrSchema));