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