Remix Validated Form

v6 is coming!

Check out the RFC to get an early look or leave a comment.

API Reference

(shape: ZodRawShape) => ZodEffects

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. (If you're expecting multiple values for a field, use repeatable.)

The primary use-case for this helper is to accept FormData, but it works with any iterable that returns entries. This means it can accept URLSearchParams or regular objects as well.

Note: If you're using remix-validated-form, you don't need this but you can use it.

Usage

You can use this the same way you would use z.object.

const schema = zfd.formData({
  field1: zfd.text(),
  field2: zfd.text(),
});

const someFormData = new FormData();
const dataObject = schema.parse(someFormData);

It's also possible to pass a zod schema to formData.

const schema = zfd.formData(
  z.object({
    field1: zfd.text(),
    field2: zfd.text(),
  })
);

(schema?: ZodTypeAny) => ZodEffects

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.

Usage

const const schema = zfd.formData({
  requiredString: zfd.text(),
  stringWithMinLength: zfd.text(z.string().min(10)),
  optional: zfd.text(z.string().optional()),
})

(schema?: ZodTypeAny) => ZodEffects

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.

Note: The preprocessing only coerces the value into a number. It doesn't use parseInt. Something like "24px" will not be transformed and will be treated as a string.

Usage

const schema = zfd.formData({
  requiredNumber: zfd.numeric(),
  numberWithMin: zfd.numeric(z.number().min(13)),
  optional: zfd.numeric(z.number().optional()),
});

(schema?: ZodTypeAny) => ZodEffects

Transforms any empty File objects to undefined before validating. This makes it so empty files will fail required checks, allowing you to use optional for optional fields. If you call zfd.file with no arguments, it will assume the field is a required file by default.

Usage

const schema = zfd.formData({
  requiredFile: zfd.file(),
  optional: zfd.file(z.instanceof(File).optional()),
});

There is a unique case in Remix when using a CustomUploadHandler, the field will be a File on the client side, but an ID string (or URL) after uploading on the server.

In this case you will need the schema to switch to string on the server:

const baseSchema = z.object({
  someOtherField: zfd.text(),
});

const clientSchema = z.formData(
  baseSchema.and({
    file: zfd.file(),
  })
);

const serverSchema = z.formData(
  baseSchema.and({
    file: z.string(),
  })
);

(params?: { trueValue?: string }) => ZodUnion

Turns the value from a checkbox field into a boolean. By default, this does not require the checkbox to be checked. For checkboxes with a value attribute, you can pass that as the trueValue option.

If you have a checkbox group and you want to leave the values as strings, repeatable might be what you want.

Usage

const schema = zfd.formData({
  defaultCheckbox: zfd.checkbox(),
  checkboxWithValue: zfd.checkbox({ trueValue: "true" }),
  mustBeTrue: zfd
    .checkbox()
    .refine((val) => val, "Please check this box"),
});

Background on native checkbox behavior

If you're used to using client-side form libraries and haven't dealt with native form behavior much, the native checkbox behavior might be non-intuitive (it was for me).

Take this checkbox:

<input name="myCheckbox" type="checkbox" />

If you check this checkbox and submit the form, the value in the FormData will be "on".

If you add a value prop:

<input
  name="myCheckbox"
  type="checkbox"
  value="someValue"
/>

Then the checked value of the checkbox will be "someValue" instead of "on".

If you leave the checkbox unchecked, the FormData will not include an entry for myCheckbox at all.

(Further reading)

(schema?: ZodTypeAny) => ZodEffects

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. This is specifically meant to work with data transformed by zfd.formData (or by remix-validated-form).

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. If you want to customize the type of the item, but don't care about validations on the array itself, you can use repeatableOfType.

Usage

const schema = zfd.formData({
  myCheckboxGroup: zfd.repeatable(),
  atLeastOneItem: zfd.repeatable(
    z.array(zfd.text()).min(1)
  ),
});

(schema?: ZodTypeAny) => ZodEffects

A convenience wrapper for repeatable. Instead of passing the schema for an entire array, you pass in the schema for the item type.

Usage

const schema = zfd.formData({
  repeatableNumberField: zfd.repeatableOfType(
    zfd.numeric()
  ),
});

(schema: ZodTypeAny) => ZodEffects

Preprocess a field where you expect a JSON string. This often happens when using a controlled component like react-select in Remix. In that case you might choose to store the entire value of the component in a hidden input encoded as json.

Unlike other helpers, providing a schema is required.

Usage

const schema = zfd.formData({
  jsonField: zfd.json(
    z.array(
      z.object({
        label: z.string(),
        value: z.string(),
      })
    )
  ),
});

(formData: unknown) => Record<string, string | string[]>

The raw preprocessor function that is used by the formData helper. You usually don't need this and is only supplied for advanced use-cases.