Validation
Define validation rules on each question to ensure respondents provide valid answers. Validation runs on page navigation and final submission, with 10 built-in rule types and unlimited custom validators.
ValidationRule Interface
interface ValidationRule {
type: ValidationRuleType;
value?: string | number | RegExp;
message?: string;
/** Custom validator function — only used when type is 'custom' */
validator?: (value: AnswerValue, question: Question, answers: SurveyAnswers) => boolean | string;
}
type ValidationRuleType =
| 'required'
| 'minLength'
| 'maxLength'
| 'pattern'
| 'min'
| 'max'
| 'email'
| 'url'
| 'phone'
| 'time'
| 'custom';
interface ValidationError {
questionId: string;
message: string;
rule: ValidationRuleType;
}All Built-in Rules
| Rule | value type | Description | Applies to |
|---|---|---|---|
required | — | Field must have a non-empty value | All types |
minLength | number | Minimum character length | text, textarea, password, email, url, phone |
maxLength | number | Maximum character length | text, textarea, password, email, url, phone |
pattern | string | RegExp | Value must match the regex pattern | text, textarea, email, url, phone |
min | number | Minimum numeric value | number, slider, rating |
max | number | Maximum numeric value | number, slider, rating |
email | — | Value must be a valid e-mail address | email, text |
url | — | Value must be a valid http / https URL | url, text |
phone | — | Value must be a valid phone number | phone, text |
time | — | Value must be a valid HH:MM or HH:MM:SS time | time, text |
custom | — | Run any validator function you provide | All types |
required
Fails when the answer is empty, null, undefined, an empty string, or an empty array. For boolean questions rendered as a checkbox, required means the box must be checked (value === true).
The top-level required: true shorthand is identical to adding { type: 'required' } inside the validation array.
{
id: 'name',
type: 'text',
label: 'Full name',
required: true, // ← shorthand (preferred)
// or equivalently:
// validation: [{ type: 'required', message: 'Please enter your name' }],
}minLength
Checks String(value).length >= value. Only runs when the field is non-empty.
{
id: 'bio',
type: 'textarea',
label: 'Short bio',
validation: [
{ type: 'minLength', value: 20, message: 'Please write at least 20 characters' },
],
}maxLength
Checks String(value).length <= value.
{
id: 'username',
type: 'text',
label: 'Username',
validation: [
{ type: 'minLength', value: 3, message: 'At least 3 characters' },
{ type: 'maxLength', value: 20, message: 'At most 20 characters' },
],
}pattern
Tests the value against a regular expression. Supply the pattern as a string (wrapped in new RegExp(...)) or as an actual RegExp object.
// String pattern
{
id: 'username',
type: 'text',
label: 'Username',
validation: [
{
type: 'pattern',
value: '^[a-zA-Z0-9_]+$',
message: 'Only letters, numbers, and underscores allowed',
},
],
}
// RegExp object
{
id: 'zip',
type: 'text',
label: 'ZIP code',
validation: [
{ type: 'pattern', value: /^d{5}(-d{4})?$/, message: 'Enter a valid ZIP code' },
],
}min
Checks Number(value) >= rule.value. Useful for number, slider, and rating questions.
{
id: 'age',
type: 'number',
label: 'Age',
required: true,
validation: [
{ type: 'min', value: 18, message: 'You must be at least 18 years old' },
],
}max
Checks Number(value) <= rule.value.
{
id: 'quantity',
type: 'number',
label: 'Quantity',
required: true,
validation: [
{ type: 'min', value: 1, message: 'Minimum order is 1' },
{ type: 'max', value: 500, message: 'Maximum order is 500' },
],
}email
Tests the value against /^[^\s@]+@[^\s@]+\.[^\s@]+$/. The email question type applies this automatically, but you can add it explicitly to any text field too.
{
id: 'contact',
type: 'email',
label: 'Contact email',
required: true,
validation: [
{ type: 'email', message: 'Please enter a valid email address' },
],
}url
Parses the value with new URL() and requires the protocol to be http: or https:. The url question type applies this automatically.
{
id: 'website',
type: 'url',
label: 'Your website',
validation: [
{ type: 'url', message: 'Must be a valid URL starting with http:// or https://' },
],
}phone
Tests against /^\+?[\d\s\-().]{7,20}$/. Accepts common formats such as +1 800 555 0199, (123) 456-7890, and +91 98765 43210. The phone question type applies this automatically.
{
id: 'mobile',
type: 'phone',
label: 'Mobile number',
required: true,
validation: [
{ type: 'phone', message: 'Enter a valid phone number (e.g. +1 800 555 0199)' },
],
}time
Tests against /^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d)?$/ — i.e. 24-hour HH:MM or HH:MM:SS. The time question type applies this automatically.
{
id: 'meeting-time',
type: 'time',
label: 'Preferred meeting time',
required: true,
validation: [
{ type: 'time', message: 'Enter a valid time in HH:MM format' },
],
}custom
Runs a validator function you supply. The function receives the current answer value and the full answers map, and must return:
true— valid, no error.false— invalid; usesrule.messageor a generic fallback.- A string — invalid; that string becomes the error message (overrides
rule.message).
validator field holds a function, which is not valid JSON. If your schema is stored as JSON (e.g. loaded from an API or database), use the validators option approach below instead.Inline validator (TypeScript / JS schema)
// schema defined in .ts / .tsx — functions are fine here
const schema: SurveySchema = {
id: 'signup',
pages: [{
id: 'p1',
questions: [
{
id: 'password',
type: 'password',
label: 'Password',
required: true,
validation: [
{ type: 'minLength', value: 8, message: 'At least 8 characters required' },
{
type: 'custom',
validator: (value) => {
const str = String(value);
if (!/[0-9]/.test(str)) return 'Must contain at least one number';
if (!/[A-Z]/.test(str)) return 'Must contain at least one uppercase letter';
if (!/[^a-zA-Z0-9]/.test(str)) return 'Must contain at least one special character';
return true;
},
},
],
},
{
id: 'confirm-password',
type: 'password',
label: 'Confirm password',
required: true,
validation: [
{
type: 'custom',
message: 'Passwords do not match',
validator: (value, _question, answers) => value === answers['password'] || 'Passwords do not match',
},
],
},
],
}],
};validators option (works with JSON schemas too)
Pass a validators map to useSurvey or SurveyRenderer. Each key is a question id; the value is the validator function. The schema itself stays pure JSON — just mark the question with { type: 'custom' } and a message.
// schema can be plain JSON — no functions needed
const schema = {
id: 'signup',
pages: [{
id: 'p1',
questions: [
{
id: 'password',
type: 'password',
label: 'Password',
required: true,
validation: [
{ type: 'minLength', value: 8, message: 'At least 8 characters required' },
{ type: 'custom', message: 'Must contain a number, uppercase letter, and special character' },
],
},
{
id: 'confirm-password',
type: 'password',
label: 'Confirm password',
required: true,
validation: [
{ type: 'custom', message: 'Passwords do not match' },
],
},
],
}],
};
// Validator logic lives here, outside the schema
<SurveyRenderer
schema={schema}
options={{
validators: {
password: (value) => {
const str = String(value);
if (!/[0-9]/.test(str)) return 'Must contain at least one number';
if (!/[A-Z]/.test(str)) return 'Must contain at least one uppercase letter';
if (!/[^a-zA-Z0-9]/.test(str)) return 'Must contain at least one special character';
return null;
},
'confirm-password': (value, _question, answers) =>
value === answers['password'] ? null : 'Passwords do not match',
},
onSubmit: async (answers) => console.log(answers),
}}
/>false): "[label] is invalid"Per-Page Validation & Programmatic Access
By default, the library validates the current page when the user navigates forward. You can also trigger validation programmatically:
const { validate, validateCurrentPage, errors, getError } = useSurvey(schema);
// Validate just the current page (returns true if no errors)
const isPageValid = validateCurrentPage();
// Validate the entire survey across all pages
const isAllValid = validate();
// errors is a flat array of ValidationError objects:
// [{ questionId: 'name', message: 'Full name is required', rule: 'required' }]
// getError returns the first error message for a question ID:
const nameError = getError('name'); // string | undefinedSchema-level Settings
Control when validation fires via the settings object on your schema:
const schema: SurveySchema = {
id: 'my-survey',
pages: [...],
settings: {
validateOnPageChange: true, // validate current page before advancing (default: true)
},
};Tips
- — Hidden questions (via
visibleIf) are automatically skipped during validation. - — If a field is empty and not
required, all other rules (length, pattern, format, etc.) are skipped. - — Rules run in array order; the first failure adds an error but the rest still execute, so multiple errors can surface at once.
- —
email,url,phone, andtimequestion types already wire up their respective format rules internally — you only need to add them explicitly if you want a custom error message. - —
customvalidators with an inlinevalidatorfunction require a TypeScript/JS schema — functions are not valid JSON. For JSON schemas, use thevalidatorsoption onuseSurvey/SurveyRendererinstead. - — Validation errors are cleared automatically when the user changes their answer.