Skip to content

optional-form-field

Edit

Makes any Zod schema optional while mapping an exact empty form value to undefined.

optional-form-field.ts
import * as z from "zod";
/**
* Creates an optional schema that treats an exact empty string as `undefined`.
*
* Other values, including whitespace-only strings, are passed to the supplied
* schema unchanged so that it remains in control of validation and transforms.
*/
export const optionalFormField = <Schema extends z.ZodType>(
schema: Schema,
): z.ZodType<
z.output<Schema> | undefined,
z.input<Schema> | "" | undefined
> =>
z.preprocess(
(value: z.input<Schema> | "" | undefined) =>
value === "" ? undefined : value,
schema.optional(),
) as z.ZodType<
z.output<Schema> | undefined,
z.input<Schema> | "" | undefined
>;
Terminal
wget -O src/schemas/optional-form-field.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/zod-schemas/optional-form-field.ts
optional-form-field.example.ts
import * as z from "zod";
import { optionalFormField } from "./optional-form-field";
const profileFormSchema = z.object({
displayName: z.string().trim().min(1),
bio: optionalFormField(z.string().trim().max(160)),
website: optionalFormField(z.httpUrl()),
notifications: optionalFormField(
z.object({
frequency: z.enum(["daily", "weekly"]),
}),
),
});
const profile = profileFormSchema.parse({
displayName: " Ada ",
bio: "",
website: "https://example.com",
notifications: {
frequency: "weekly",
},
});
// {
// displayName: "Ada",
// bio: undefined,
// website: "https://example.com",
// notifications: { frequency: "weekly" }
// }
console.log(profile);
const invalid = profileFormSchema.safeParse({
displayName: "Ada",
notifications: {
frequency: "monthly",
},
});
if (!invalid.success) {
// ["notifications", "frequency"] — the inner schema's direct issue path.
console.log(invalid.error.issues[0]?.path);
}
export type ProfileFormInput = z.input<typeof profileFormSchema>;
export type Profile = z.output<typeof profileFormSchema>;
  • schema (Schema extends z.ZodType) — The schema used for every non-empty value.

A schema with input type z.input<Schema> | "" | undefined and output type z.output<Schema> | undefined. Zod 4 preprocessing maps the exact empty string "" to undefined before the optional inner schema runs. An already undefined value follows the same absent-value path.

Whitespace-only strings are not considered empty. All other values are passed to the supplied schema unchanged, preserving its validation, transforms, asynchronous parsing, and output type. Inner failures remain direct Zod issues with their original codes and nested paths instead of being wrapped in an invalid_union issue. If the supplied schema also accepts "", the empty-value rule takes precedence and still returns undefined.

Both "" and undefined short-circuit defaults declared inside schema. Apply a default or fallback around the returned field when absent input should produce a value.