Skip to main content

Validation

All input components accept a validate prop. Pass a single validator function or an array of validators; errors are displayed automatically below the input.

import { TextInput, required, minLength } from '@strato-admin/admin';

<TextInput source="title" validate={required()} />
<TextInput source="slug" validate={[required(), minLength(3)]} />

Built-in Validators

required(message?)

Fails when the value is empty (null, undefined, '', or []).

<TextInput source="name" validate={required()} />
<TextInput source="name" validate={required('Name is required')} />

The validated field is also marked with a required indicator in the form UI. This is the same effect as setting isRequired on a ResourceSchema field.

minLength(min, message?)

Fails when the string length is less than min. Passes when the value is empty (combine with required() if the field is mandatory).

<TextInput source="password" validate={minLength(8)} />
<TextInput source="password" validate={[required(), minLength(8, 'At least 8 characters')]} />

maxLength(max, message?)

Fails when the string length exceeds max.

<TextInput source="bio" validate={maxLength(280)} />

minValue(min, message?)

Fails when the numeric value is less than min. Passes when empty.

<NumberInput source="quantity" validate={minValue(1)} />

maxValue(max, message?)

Fails when the numeric value exceeds max. Passes when empty.

<NumberInput source="discount" validate={maxValue(100)} />
<NumberInput source="price" validate={[minValue(0), maxValue(10000)]} />

number(message?)

Fails when the value cannot be parsed as a number. Useful on <TextInput> when you want numeric content without a dedicated number field.

<TextInput source="zip_code" validate={number()} />

email(message?)

Fails when the value is not a valid email address. Passes when empty.

<TextInput source="email" validate={[required(), email()]} />

regex(pattern, message)

Fails when the string does not match the regular expression. Passes when empty.

<TextInput source="zip" validate={regex(/^\d{5}$/, 'Must be a 5-digit ZIP code')} />

choices(list, message?)

Fails when the value is not in the provided list. Most useful for custom inputs; <SelectInput> validates choices automatically.

<TextInput source="status" validate={choices(['draft', 'published', 'archived'])} />

Combining Validators

Pass an array to apply multiple validators. They run in order and stop at the first failure.

import { TextInput, required, minLength, maxLength } from '@strato-admin/admin';

<TextInput source="username" validate={[required(), minLength(3), maxLength(20)]} />;

Custom Validators

A validator is a function with the signature (value, allValues) => string | undefined. Return an error message string to fail, or undefined to pass.

const mustBePositive = (value) => (value > 0 ? undefined : 'Must be a positive number');

<NumberInput source="price" validate={mustBePositive} />;

You can access the entire form state via the second argument:

const passwordsMatch = (value, allValues) => (value !== allValues.password ? 'Passwords do not match' : undefined);

<TextInput source="password_confirm" validate={passwordsMatch} />;

Async Validators

Return a Promise to perform asynchronous validation (e.g. an API check). The input shows a loading state while the promise is pending.

const isSlugAvailable = async (value) => {
const res = await fetch(`/api/check-slug?slug=${value}`);
const { available } = await res.json();
return available ? undefined : 'This slug is already taken';
};

<TextInput source="slug" validate={[required(), isSlugAvailable]} />;
Memoize inline validators

Defining a validator function directly in JSX creates a new function reference on every render, which can cause performance issues. Either define validators outside the component or use the built-in memoized helpers. The built-in validators (required(), minLength(), etc.) are already memoized.

useUnique — Uniqueness Validation

useUnique is a hook that returns an async validator checking that no other record in the resource has the same value for a field. It calls dataProvider.getList with a filter under the hood.

import { useUnique, TextInput } from '@strato-admin/admin';

const UserCreate = () => {
const unique = useUnique();

return <TextInput source="username" validate={[required(), unique()]} />;
};

Options

Options can be passed to useUnique() at declaration time or to the returned unique() call. Call-time options take precedence.

// At declaration time
const unique = useUnique({ message: 'This email is already registered' });
<TextInput source="email" validate={unique()} />;

// At call time
const unique = useUnique();
<TextInput source="email" validate={unique({ message: 'Already taken' })} />;
OptionTypeDescription
messagestringError message when uniqueness fails. Default: 'ra.validation.unique'
filterobjectAdditional filters applied to the uniqueness check (e.g. scoping by tenant).
debouncenumberMilliseconds to debounce the API call. Default: 1000
resourcestringOverride the resource to check against. Defaults to the current resource context.

Scoping uniqueness with a filter:

const unique = useUnique();

// username unique within the user's organization
<TextInput source="username" validate={unique({ filter: { organization_id: formData.organization_id } })} />;

Form-Level Validation

Instead of per-field validators, you can validate the whole form at once by passing a validate function to <Create> or <Edit>. Return an object mapping field names to error messages.

const validateOrder = (values) => {
const errors: Record<string, string> = {};
if (!values.customer_id) errors.customer_id = 'Required';
if (values.quantity < 1) errors.quantity = 'Must order at least one item';
return errors;
};

<Create validate={validateOrder}>...</Create>;

Form-level validation runs on every submit attempt. Per-field and form-level validation can be used together.