Form Validation

Framework:

Schema FormX provides a built-in validation mechanism based on field configuration. This chapter explains how to configure validation rules and use custom validators.

Required Field Validation

Set the required property in the field configuration to validate required fields.

Validation Rules

Required Field (required)

const schema = [
  {
    type: 'string',
    name: 'username',
    label: 'Username',
    required: true  // Required field
  }
];

Empty Value Rules by Field Type

Required validation checks for empty values based on field type:

Field TypeEmpty ValuesDefault Error Message
stringnull, undefined, ''${label}不能为空
numbernull, undefined, ''${label}不能为空
booleannull, undefined, ''${label}不能为空
objectnull, undefined, {}${label}不能为空
arraynull, undefined, []${label}不能为空

Note: The default error message is hardcoded in Chinese (${label}不能为空, meaning "${label} cannot be empty"). To display English error messages, use a custom validator function instead of the required property.

Important Notes:

  • false for boolean type is NOT empty - required validation passes
  • 0 for number type is NOT empty - required validation passes
  • Empty string '' is considered empty for all types
  • Hidden fields (hidden: true) still participate in validation — if a hidden field has required: true, it will still trigger validation errors. Use conditional required or a custom validator to skip validation for hidden fields
  • If you need more complex required validation, use a custom validator

Validation Logic Source Code:

// Core logic for required validation
if (
  required &&
  (value === null ||
    value === undefined ||
    value === '' ||
    (Array.isArray(value) && value.length === 0) ||
    isEqual(value, {}))
) {
  errors[name] = `\${label}不能为空`;  // Chinese: "\${label} cannot be empty"
}

Field Default Values

Form automatically sets default values based on field type:

Field TypeDefault Value
string''
numbernull
booleannull
object{}
array[]

Override defaults using defaultData:

<Form 
  schema={schema}
  defaultData={{
    name: 'John',       // Override string default
    age: 25,            // Override number default
    agree: true         // Override boolean default
  }}
/>

Custom Validator (validator)

Use the validator function for custom validation.

Basic Usage

const schema = [
  {
    type: 'string',
    name: 'email',
    label: 'Email',
    required: true,
    validator: ({ value, label }) => {
      if (!value) return `\${label} is required`;
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
        return `\${label} format is incorrect`;
      }
      return undefined; // Validation passed
    }
  }
];

Validator Parameters

type Validator = (params: {
  value: unknown;               // Current field value
  label: string;                // Field display name
  data: Readonly<Record<string, unknown>>; // Entire form data
}) => undefined | string | Promise<undefined | string>;
  • undefined means validation passed
  • string means validation failed, returning the error message

Async Validation

const schema = [
  {
    type: 'string',
    name: 'username',
    label: 'Username',
    required: true,
    validator: async ({ value, label }) => {
      if (!value) return `\${label} is required`;
      
      // Simulate API call
      const isAvailable = await checkUsernameAvailability(value);
      if (!isAvailable) return `\${label} is already taken`;
      
      return undefined;
    }
  }
];

Multiple Validation Rules

const schema = [
  {
    type: 'string',
    name: 'password',
    label: 'Password',
    required: true,
    validator: ({ value, label }) => {
      if (!value) return `\${label} is required`;
      if (value.length < 8) return `\${label} must be at least 8 characters`;
      if (!/[A-Z]/.test(value)) return `\${label} must contain at least one uppercase letter`;
      if (!/[0-9]/.test(value)) return `\${label} must contain at least one digit`;
      if (!/[!@#$%^&*]/.test(value)) return `\${label} must contain at least one special character`;
      return undefined;
    }
  }
];

Validation Triggering

Automatic Validation

Validation is automatically triggered in the following situations:

  1. Field value change: When user modifies a field value
  2. After dependency linkage: When dependency changes trigger linkage, related fields are also validated

Manual Validation Call

Use the validate() method to manually trigger validation:

const formRef = useRef<FormInstance>(null);

const handleSubmit = async () => {
  const { data, errors } = await formRef.current.validate();
  
  if (errors) {
    console.log('Validation failed:', errors);
    return;
  }
  
  console.log('Validation passed:', data);
  // Submit data...
};

<Form ref={formRef} schema={schema} components={components} />
<button onClick={handleSubmit}>Submit</button>

validate() Method Details

The validate() method can be used to manually trigger form validation:

Usage Examples:

// Validate all fields
const result = await formRef.current.validate();

Exception Handling

Exceptions thrown by validators are automatically caught and converted to error messages:

{
  name: 'field',
  label: 'Field',
  validator: ({ value }) => {
    // Exception will be caught
    if (value === 'invalid') {
      throw new Error('Invalid value');
    }
    return undefined;
  }
}

// Exception is converted to:
// errors: { field: 'Invalid value' }

Common Validation Patterns

Length Validation

{
  type: 'string',
  name: 'username',
  label: 'Username',
  validator: ({ value, label }) => {
    if (!value) return `\${label} is required`;
    if (value.length < 3) return `\${label} must be at least 3 characters`;
    if (value.length > 20) return `\${label} must be at most 20 characters`;
    return undefined;
  }
}

Format Validation

// Email validation
{
  type: 'string',
  name: 'email',
  label: 'Email',
  validator: ({ value, label }) => {
    if (!value) return `\${label} is required`;
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      return `\${label} format is incorrect`;
    }
    return undefined;
  }
}

// URL validation
{
  type: 'string',
  name: 'website',
  label: 'Website',
  validator: ({ value, label }) => {
    if (value && !/^https?:\/\/.+\..+/.test(value)) {
      return `\${label} format is incorrect`;
    }
    return undefined;
  }
}

// International phone number validation (e.g., +1-234-567-8901)
{
  type: 'string',
  name: 'phone',
  label: 'Phone',
  validator: ({ value, label }) => {
    if (value && !/^\+?[\d\s\-()]{7,15}$/.test(value)) {
      return `\${label} format is incorrect`;
    }
    return undefined;
  }
}

// Chinese mainland phone number validation (11 digits starting with 1)
{
  type: 'string',
  name: 'phone',
  label: 'Phone',
  validator: ({ value, label }) => {
    if (value && !/^1[3-9]\d{9}$/.test(value)) {
      return `\${label} format is incorrect`;
    }
    return undefined;
  }
}

Range Validation

{
  type: 'number',
  name: 'age',
  label: 'Age',
  validator: ({ value, label }) => {
    if (value === null || value === undefined) return `\${label} is required`;
    if (value < 0 || value > 120) return `\${label} must be between 0 and 120`;
    return undefined;
  }
}

Cross-Field Validation

// Password confirmation
{
  type: 'string',
  name: 'confirmPassword',
  label: 'Confirm Password',
  validator: ({ value, label, data }) => {
    if (!value) return `\${label} is required`;
    if (value !== data.password) return 'Passwords do not match';
    return undefined;
  }
}

Validation Error Display

Validation errors can be retrieved via the validate() method:

const formRef = useRef(null);
const [validationErrors, setValidationErrors] = useState(null);

const handleValidate = async () => {
  const { errors } = await formRef.current.validate();
  setValidationErrors(errors);
};

// Display errors
{validationErrors && (
  <div>
    {Object.entries(validationErrors).map(([name, message]) => (
      <div key={name}>{message}</div>
    ))}
  </div>
)}

Note: The onChange callback only returns { data, name, value }, not errors. To get validation results in real-time, call the validate() method.


## Best Practices

### Error Messages

- Provide clear, actionable error messages
- Include the field name for context
- Use consistent error message format

### Validation Logic

- Keep validators focused on single validation concerns
- Return `undefined` (not `null`) to indicate success
- Provide specific error messages

### Performance

- Avoid synchronous operations in async validators
- Use debouncing for expensive validation operations
- Consider caching validation results

## Related Documentation

- [Form Configuration](/en/guide/basic/form-config) - Schema structure
- [Custom Components](/en/guide/advanced/custom-field) - Custom form components
- [Dependency Linkage](/en/guide/advanced/dependency) - Dependency linkage