Field widgets already reject the obvious problems — a required text field left empty, a number field given letters. Entity-level validation is for everything else: rules that depend on the entity as a whole, or on data outside any single field, like uniqueness checks or cross-field consistency.
What you'll learn in this lesson
- Where entity-level validation actually runs, relative to field-level validation
- How to override
validateForm()on aContentEntityFormsubclass - The difference between validating a single field and validating the entity as a whole
- How constraint-based validation (via the Entity Validation API) differs from form-level checks
Overriding validateForm() on an entity form
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$name = $form_state->getValue(['name', 0, 'value']);
$existing = \Drupal::entityTypeManager()
->getStorage('content_entity_example_contact')
->loadByProperties(['name' => $name]);
$current_id = $this->entity->id();
foreach ($existing as $entity) {
if ($entity->id() != $current_id) {
$form_state->setErrorByName('name', $this->t('A contact with this name already exists.'));
break;
}
}
}
Calling parent::validateForm() first matters: the base class runs each field widget's own validation (required checks, type checks, any #element_validate callbacks) before your custom logic runs. Skip that call and you silently lose all of the field-level validation the form was already doing for free.
The uniqueness check itself is a genuinely entity-level concern — no single field widget can know "does another entity already have this value," because that requires querying storage, not just inspecting the submitted value in isolation.
$entity->id() != $current_id check above is what stops the edit form from flagging a contact's own unchanged name as a duplicate of itself. Forget this and every edit of an existing, unmodified contact starts failing validation the moment you save it.The other path: entity constraints
Drupal also has a separate, lower-level Entity Validation API (\Drupal\Core\Entity\EntityConstraintViolationListInterface, driven by Symfony Validator constraints attached to field definitions). Form-level validateForm() overrides, like the one above, are the more common and more direct approach for form-specific business rules; entity constraints matter more when you need the same rule enforced even for entities saved outside any form — a migration script or a REST API write, for instance.
See it for yourself
If you have content_entity_example enabled, create two contacts with the same name via the add form and observe Drupal's default behavior, then compare it against what a uniqueness override like the one above would add.
Quick check: why does the code above compare against
$this->entity->id()rather than just checking "does any contact have this name"? (Because on the edit form, the entity being edited is itself one of the entities that already has that name — without excluding it by ID, saving an untouched existing contact would always trigger the duplicate error.)
Key takeaways
- Always call
parent::validateForm()first when overriding it on an entity form — it runs all the field-level validation you'd otherwise lose. - Entity-level validation is for rules that need more than one field's value, or that need to query other entities (like uniqueness).
- On edit forms, exclude the entity's own ID from any "does this already exist" check, or every unmodified save will fail.
- The Entity Validation API (constraints) is a separate, lower-level mechanism worth reaching for only when a rule must hold even outside form submissions.
Coming up next
Once a submission passes validation, something has to actually persist it. Next, we look at exactly what happens inside an entity form's submitForm()/save() cycle.