Using hook_form_alter() to modify validationfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Every validation technique so far has assumed you own the form — you're writing its class, its buildForm(), its validateForm(). But what if the form belongs to someone else entirely? Drupal core's login form, a contributed module's settings form, even a form from a module you can't edit — hook_form_alter() lets any module reach in and change another form's validation behavior from the outside, with zero access to that form's original source.

What you'll learn in this lesson

  • How hook_form_alter() gives you a chance to modify any form, including its validation, right before it renders
  • How to add a validation callback to a form you don't own
  • How to target one specific form with hook_form_FORM_ID_alter() instead of every form on the site
  • How to remove or reorder an existing validation callback that's already on the form

Adding a validator to someone else's form

/**
 * Implements hook_form_FORM_ID_alter().
 */
function mymodule_form_user_login_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
  $form['#validate'][] = 'mymodule_login_form_validate';
}

function mymodule_login_form_validate(array &$form, FormStateInterface $form_state) {
  $name = $form_state->getValue('name');
  if (str_contains($name, ' ')) {
    $form_state->setErrorByName('name', t('Usernames cannot contain spaces.'));
  }
}

$form['#validate'] is an array of callable validation functions Drupal runs during the validation phase — appending to it with [] = adds your function without disturbing the form's own existing validators. This is the alter-hook equivalent of overriding validateForm() on a form you actually own: same validation phase, same $form_state, just attached from outside instead of from within the form class itself.

hook_form_alter() vs. hook_form_FORM_ID_alter()

The generic hook_form_alter(array &$form, FormStateInterface $form_state, $form_id) runs for every single form rendered anywhere on the site — you'd need an if ($form_id === 'user_login_form') check inside it to target just one form. hook_form_FORM_ID_alter(), as used above, does that filtering for you at the function-name level: Drupal only invokes mymodule_form_user_login_form_alter() when the form actually being built is user_login_form. Prefer the specific variant whenever you know exactly which form you're targeting — it's both clearer and cheaper, since Drupal doesn't even call your function for forms you don't care about.

Removing or reordering an existing validator

$key = array_search('some_module_old_validate', $form['#validate']);
if ($key !== FALSE) {
  unset($form['#validate'][$key]);
}

$form['#validate'] is just a plain PHP array at this point in the lifecycle — you can inspect it, remove entries you don't want, or reorder them, with the same array functions you'd use on any other array. This is a genuinely powerful (and genuinely risky) capability: you're editing another module's validation behavior, so change it only when you're confident about why the existing validator needs to go.

Alter hooks run late, close to render time. By the time your hook_form_alter() runs, the form has already been fully built by its owning code. Your additions to #validate are appended at the end of that array, meaning your custom validator always runs after the form's own built-in validation — you can't validate something before the form's own author's checks have already run.

See it for yourself

Implement hook_form_user_login_form_alter(), append a validator rejecting usernames containing a space, and confirm it fires on the real core login form — a form your module never wrote a single line of.

Quick check: you implement the generic hook_form_alter() instead of the specific hook_form_user_login_form_alter(), and forget the if ($form_id === 'user_login_form') guard inside it. What actually happens? (Answer: your alter logic runs on every single form rendered anywhere on the site, not just the login form — including forms where the change makes no sense or could break something. Always guard a generic hook_form_alter() with a form_id check, or use the FORM_ID-specific variant instead.)

Key takeaways

  • hook_form_alter() and hook_form_FORM_ID_alter() let any module modify another form's structure and validation, without owning that form's original code.
  • Append to $form['#validate'][] to add a new validation callback to a form you don't own.
  • Prefer hook_form_FORM_ID_alter() over the generic hook plus a manual $form_id check — Drupal only invokes it for the one form you're targeting.
  • $form['#validate'] is a plain array — you can remove or reorder existing validators, not just add new ones.
  • Alter-hook validators always run after the form's own built-in validation, since alter hooks fire after the form is already fully built.

Coming up next

Validation only tells you whether input is acceptable — it doesn't do anything with it. Next, the tutorial moves into the submission phase: what happens the moment a fully validated form is actually submitted, starting with handling the submission itself.