Why Use Validation Constraints Over Form Validation

Why Use Validation Constraints Over Form Validation

Sam Rollin
Sam Rollin
January 10, 2026
Last updated : February 15, 2026
January 10, 2026

If you've built Drupal sites for any length of time, you've probably written plenty of validateForm() methods. They work, they're familiar, and they get the job done for basic form checks. But as Drupal has matured, especially with Drupal 11's tighter Symfony integration, there's a better place for most of your validation logic: validation constraints. This isn't about form validation being wrong or deprecated. It's about understanding which tool fits which job, and why constraints have become the preferred approach for anything related to actual data integrity. Let's break down what each approach does, when to use which, and how to make the right call for your projects.


Understanding the Two Validation Paths in Drupal 11

Drupal 11 offers two distinct mechanisms for checking whether data is valid. They serve different purposes and operate at different layers of the system.

Form Validation: The UI Layer

Form validation happens inside the form lifecycle. When a user submits a form, Drupal's FormBuilder calls validateForm() on your form class, runs any #element_validate callbacks you've defined, and sets errors that prevent submission if something's wrong.

This process is tied entirely to that specific form. The validation logic lives in the form class, runs only during form submission, and has no effect anywhere else in your system.

Here's what that typically looks like:

public function validateForm(array &$form, FormStateInterface $form_state) {
  $email = $form_state->getValue('email');
  if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $form_state->setErrorByName('email', $this->t('Please enter a valid email address.'));
  }
}

Simple enough. But what happens when that same data arrives through a REST API? Or gets imported via a migration? That validation doesn't run.

Validation Constraints: The Data Layer

Constraints work differently. They're attached to data definitions (entity fields, typed data properties, configuration schemas) rather than to forms. When you call $entity->validate(), Drupal walks through every field and property, checking each one against its defined constraints.

This approach builds on Symfony's Validator component, which Drupal has integrated through its Typed Data API. A constraint consists of two parts: a constraint class that defines the rule and its error message, and a validator class that implements the actual checking logic.

// The constraint definition
namespace Drupal\my_module\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;

/**
 * @Constraint(
 *   id = "UniqueProductSku",
 *   label = @Translation("Unique product SKU")
 * )
 */
class UniqueProductSkuConstraint extends Constraint {
  public $message = 'The SKU %value is already in use.';
}

// The validator that does the work
namespace Drupal\my_module\Plugin\Validation\Constraint;

use Symfony\Component\Validator\ConstraintValidator;

class UniqueProductSkuConstraintValidator extends ConstraintValidator {
  public function validate($value, Constraint $constraint) {
    // Check if SKU exists in database
    if ($this->skuExists($value)) {
      $this->context->addViolation($constraint->message, ['%value' => $value]);
    }
  }
}

Once this constraint is attached to a field, it runs regardless of how the data arrives: forms, API calls, migrations, programmatic creation. One definition, consistent enforcement.

Why Constraints Make More Sense for Business Rules

The shift toward constraints isn't arbitrary. It reflects how Drupal is actually used in 2025.

Single Source of Truth

When you put validation logic in a constraint, you define it once. That "product SKU must be unique and match this pattern" rule lives in one place and applies everywhere. Add a new form that edits products? The constraint still runs. Build a custom import script? Same rules apply. Expose the entity through JSON:API? Still validated.

With form validation, you end up copying logic between forms, or worse, forgetting to add it to new forms entirely. We've all inherited projects where the admin form validates something the public form doesn't, and nobody noticed until bad data started appearing.

API and Headless Architectures

Many Drupal 11 projects don't rely primarily on Drupal's forms. React or Vue frontends submit data through JSON:API. Mobile apps use REST endpoints. External systems push content through custom integrations.

None of these touch Drupal's form layer. If your validation lives in validateForm(), it simply doesn't exist for these use cases. Constraints, by contrast, can be checked anywhere you have access to the entity or typed data object.

This isn't a theoretical concern. We've cleaned up plenty of projects where the web forms worked perfectly but the API allowed invalid data through because nobody thought to duplicate the validation rules.

Cleaner Architecture

Forms already have a lot of responsibility: building the render array, handling multi-step flows, processing submissions, managing redirects. Adding business rule validation on top makes form classes bloated and harder to test.

Constraints keep "is this data valid?" separate from "how does this form work?" You can unit test a constraint validator in isolation. You can reuse it across entity types. You can document it as the authoritative definition of what valid data looks like.

Alignment with Drupal's Direction

Core has moved steadily toward constraint-based validation since Drupal 8. The Entity Validation API documentation explicitly describes constraints as decoupled from form validation and encourages putting custom logic in constraint classes.

Drupal 11's integration with Symfony 7.x means you're using a current, actively maintained validation component. The ecosystem of Symfony constraints (NotNull, Length, Range, Email, and many others) works out of the box alongside Drupal's own additions.

When Form Validation Still Makes Sense

This doesn't mean form validation is obsolete. There are legitimate cases where validating at the form level is the right call.

UI-Specific Checks

Some validations only make sense in the context of a particular form. "Confirm your password matches" checks belong in the registration form, not on the user entity. A multi-step wizard might need to ensure certain fields are filled before advancing, even if those fields aren't technically required on the final entity.

Conditional Field Requirements

Forms often have conditional logic: show field B only if checkbox A is checked, make field C required only when dropdown D has a specific value. These conditions might be purely presentational or apply only to this particular workflow, not to the underlying data model.

Session or Context-Dependent Rules

Sometimes validation depends on the current user's session state, which step they're on in a process, or information that only exists during the form interaction. These checks can't easily be expressed as data-level constraints.

Our experience shows that about 80% of validation logic belongs at the constraint level, with only form-specific UX concerns staying in validateForm(). The key question is: "Would this rule need to apply if the data arrived through a different channel?" If yes, it's a constraint. If it's purely about this form's user experience, keep it in the form.

Making the Decision: A Practical Framework for Drupal Validation

When you're adding validation to a Drupal 11 project, run through these questions:

Does this rule apply to the data itself, or just to this form? If a product SKU must always be unique, that's a data rule, use a constraint. If you need users to confirm their email address matches what they typed, that's a form flow concern.

Could this data arrive through a non-form channel? API endpoints, migrations, Drush commands, programmatic entity creation, if any of these apply now or might apply later, put the validation in a constraint.

Is this rule about data integrity or user experience? "Field must not be empty" is often data integrity. "Show an inline warning when the user hasn't filled this optional but recommended field" is UX.

Would I need to duplicate this logic in another form? If you're reaching for copy-paste, that's a sign the logic belongs in a constraint instead.

We've found that applying these questions consistently leads to cleaner codebases. Constraints handle the "what is valid data" question, forms handle the "how do users interact with this" question, and there's minimal overlap between them.

Implementing Constraints in Practice

Attaching a constraint to an entity field in Drupal 11 follows a consistent pattern:

// In hook_entity_base_field_info_alter() or similar
function my_module_entity_base_field_info_alter(&$fields, $entity_type) {
  if ($entity_type->id() === 'node' && isset($fields['field_product_sku'])) {
    $fields['field_product_sku']->addConstraint('UniqueProductSku');
  }
}

For field definitions in config, you can add constraints through the field's settings. For custom entity types, define constraints directly in your baseFieldDefinitions() method.

The key is calling $entity->validate() before saving when you're working programmatically:

$violations = $entity->validate();
if ($violations->count() > 0) {
  foreach ($violations as $violation) {
    // Handle or log the violation
    $this->logger->error($violation->getMessage());
  }
  throw new ValidationException('Entity failed validation.');
}
$entity->save();

Forms that use entity form classes already integrate with this system, violations from constraints will appear as form errors automatically. For custom forms or API handlers, you'll need to check violations explicitly and surface them appropriately.

Common Mistakes to Avoid

Duplicating logic in both places. If you have a constraint and also check the same rule in validateForm(), you'll confuse future developers and risk the implementations drifting apart. Pick one location.

Forgetting to call validate() in custom code. Constraints don't automatically prevent $entity->save() from running. If you're creating entities programmatically without validation, bad data can still slip through. Build validation into your save workflows.

Over-constraining at the data level. Not everything needs to be a hard constraint. Some fields might be "recommended" rather than "required," and that's a UX concern better handled in form hints and warnings than as validation errors.

Ignoring the built-in constraints. Before writing a custom constraint, check what Drupal and Symfony already provide. Length, Range, NotBlank, Email, Regex, and many others are ready to use.

Wrapping Up

The choice between validation constraints and form validation isn't really a contest, they serve different purposes. Constraints handle data integrity rules that must apply everywhere your entities are created or modified. Form validation handles UI-specific concerns tied to particular user workflows.

For Drupal 11 projects, especially those with API exposure or multiple data entry points, treating constraints as the primary validation mechanism leads to more consistent, maintainable, and reliable applications. Form validation remains useful for what it's designed for: managing the user's experience within a specific form.

Teams we work with report that shifting business rules to constraints typically reduces validation bugs and makes the codebase easier to reason about. It takes some adjustment if you're used to putting everything in validateForm(), but the payoff in data quality and architectural clarity is worth it.

If you're planning a Drupal 11 build or refactoring an existing site's validation logic, we can help you identify which rules belong at the constraint level versus the form level, and set up a validation architecture that protects your data across every channel. Reach out to discuss your project's specific needs.

Share this article