Adding custom validation in form altersfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Adding a field via an alter hook is only useful if you can also validate it — and Drupal lets you attach a validation callback to a field you didn't even define, from a module that isn't the form's own. The mechanism is #element_validate, and it works exactly the same whether the element was written by the form's original class or bolted on by your alter hook a moment ago.

What you'll learn in this lesson

  • How to attach an #element_validate callback to any element from inside an alter hook
  • The exact signature Drupal expects that callback to have
  • How to add a whole-form validation handler via $form['#validate'] instead of a single-element one

The pattern

/**
 * Implements hook_form_FORM_ID_alter() for user_register_form.
 */
function mymodule_form_user_register_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  $form['phone_number']['#element_validate'][] = 'mymodule_validate_phone_number';
}

/**
 * Element validate callback for the phone_number field.
 */
function mymodule_validate_phone_number(array &$element, \Drupal\Core\Form\FormStateInterface $form_state, array &$complete_form) {
  $value = $element['#value'];
  if (!preg_match('/^\+?[0-9\s\-]{7,15}$/', $value)) {
    $form_state->setError($element, t('Please enter a valid phone number.'));
  }
}

How it works

#element_validate is an array, not a single value

$form['phone_number']['#element_validate'][] = 'mymodule_validate_phone_number';

Notice the [] = — this appends to the array rather than overwriting it. That matters because the element might already have its own validate callbacks (from its original definition, or from another module's alter hook running earlier). Overwriting with = instead of appending with [] = would silently delete any validation logic that was already there.

The callback signature is fixed

Drupal calls every element-validate callback with exactly three arguments, in this order: the element itself (by reference, so you could also modify it), the current $form_state, and the complete top-level $form array — useful when your validation logic needs to check the value of some other field. Getting this signature wrong causes a fatal error the moment the callback is invoked.

Setting the error

$form_state->setError($element, t('Please enter a valid phone number.'));

Passing the $element array (not a string field name) is what makes Drupal attach this error message to the right field in the rendered form — Drupal reads the element's own #parents to know where in the form structure to display it.

Whole-form validation via #validate

$form['#validate'][] = 'mymodule_validate_registration_form';
Use #element_validate when the rule only concerns one field's own value in isolation. Use $form['#validate'][] (appended the same way, on the top-level $form array) when your validation needs to compare two or more fields against each other — like confirming a password field matches its confirmation field.

See it for yourself

Submit the form with an obviously invalid phone number (letters, or too short) and confirm the error message appears directly under the phone number field, exactly as if the form's own class had defined the validation itself.

Quick check: why append to #element_validate with [] = instead of overwriting it with =? (Answer: because the element may already carry validate callbacks from its original definition or from another module's earlier alter hook — overwriting would silently discard them, breaking validation that other code depends on.)

Key takeaways

  • #element_validate attaches a validation callback to any single element, added or original, from an alter hook.
  • Always append with [] =, never overwrite with = — other code may already have callbacks registered on that element.
  • A validate callback's signature is fixed: (&$element, FormStateInterface $form_state, &$complete_form).
  • Use $form['#validate'][] instead when your logic needs to compare multiple fields against each other, not just validate one in isolation.

Coming up next

Validation stops bad data from being submitted — but what happens once a form does pass validation? Next, we attach a custom submit handler from an alter hook, so your module can react to a successful submission on a form it doesn't own.