diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index f554197c635ca741eaf6ef49e1b7343244e98bfe..aae28963b119978ae1e7fa306b4f2e42330125f9 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -151,6 +151,20 @@ abstract class ContentEntityBase extends Entity implements \IteratorAggregate, C
    */
   protected $translatableEntityKeys = array();
 
+  /**
+   * Whether entity validation was performed.
+   *
+   * @var bool
+   */
+  protected $validated = FALSE;
+
+  /**
+   * Whether entity validation is required before saving the entity.
+   *
+   * @var bool
+   */
+  protected $validationRequired = FALSE;
+
   /**
    * Overrides Entity::__construct().
    */
@@ -331,6 +345,23 @@ public function isTranslatable() {
     return !empty($bundles[$this->bundle()]['translatable']) && !$this->getUntranslated()->language()->isLocked() && $this->languageManager()->isMultilingual();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function preSave(EntityStorageInterface $storage) {
+    // An entity requiring validation should not be saved if it has not been
+    // actually validated.
+    if ($this->validationRequired && !$this->validated) {
+      // @todo Make this an assertion in https://www.drupal.org/node/2408013.
+      throw new \LogicException('Entity validation was skipped.');
+    }
+    else {
+      $this->validated = FALSE;
+    }
+
+    parent::preSave($storage);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -341,10 +372,26 @@ public function preSaveRevision(EntityStorageInterface $storage, \stdClass $reco
    * {@inheritdoc}
    */
   public function validate() {
+    $this->validated = TRUE;
     $violations = $this->getTypedData()->validate();
     return new EntityConstraintViolationList($this, iterator_to_array($violations));
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isValidationRequired() {
+    return (bool) $this->validationRequired;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValidationRequired($required) {
+    $this->validationRequired = $required;
+    return $this;
+  }
+
   /**
    * Clear entity translation object cache to remove stale references.
    */
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityConfirmFormBase.php b/core/lib/Drupal/Core/Entity/ContentEntityConfirmFormBase.php
index 887fcae3676ccdce8217e5398d8174e3c08565bd..9ca52212858b5aff18b10dcebef4e71c1d55045e 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityConfirmFormBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityConfirmFormBase.php
@@ -86,9 +86,6 @@ protected function actions(array $form, FormStateInterface $form_state) {
       'submit' => array(
         '#type' => 'submit',
         '#value' => $this->getConfirmText(),
-        '#validate' => array(
-          array($this, 'validate'),
-        ),
         '#submit' => array(
           array($this, 'submitForm'),
         ),
@@ -121,9 +118,10 @@ public function delete(array $form, FormStateInterface $form_state) {}
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
+  public function validateForm(array &$form, FormStateInterface $form_state) {
     // Override the default validation implementation as it is not necessary
     // nor possible to validate an entity in a confirmation form.
+    return $this->entity;
   }
 
 }
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
index 2c1604bb9b8849e6583cf098be5d8de278c02118..1fd99dcbc21de4159b609f248142dfab4ac1d54f 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -62,19 +62,30 @@ public function form(array $form, FormStateInterface $form_state) {
     return $form;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function buildEntity(array $form, FormStateInterface $form_state) {
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = parent::buildEntity($form, $form_state);
+
+    // Mark the entity as requiring validation.
+    $entity->setValidationRequired(!$form_state->getTemporaryValue('entity_validated'));
+
+    return $entity;
+  }
+
   /**
    * {@inheritdoc}
    *
-   * Note that extending classes should not override this method to add entity
-   * validation logic, but define further validation constraints using the
-   * entity validation API and/or provide a new validation constraint if
-   * necessary. This is the only way to ensure that the validation logic
-   * is correctly applied independently of form submissions; e.g., for REST
-   * requests.
-   * For more information about entity validation, see
-   * https://www.drupal.org/node/2015613.
+   * Button-level validation handlers are highly discouraged for entity forms,
+   * as they will prevent entity validation from running. If the entity is going
+   * to be saved during the form submission, this method should be manually
+   * invoked from the button-level validation handler, otherwise an exception
+   * will be thrown.
    */
-  public function validate(array $form, FormStateInterface $form_state) {
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
     $entity = $this->buildEntity($form, $form_state);
 
@@ -87,10 +98,10 @@ public function validate(array $form, FormStateInterface $form_state) {
 
     $this->flagViolations($violations, $form, $form_state);
 
-    // @todo Remove this.
-    // Execute legacy global validation handlers.
-    $form_state->setValidateHandlers([]);
-    \Drupal::service('form_validator')->executeValidateHandlers($form, $form_state);
+    // The entity was validated.
+    $entity->setValidationRequired(FALSE);
+    $form_state->setTemporaryValue('entity_validated', TRUE);
+
     return $entity;
   }
 
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityFormInterface.php b/core/lib/Drupal/Core/Entity/ContentEntityFormInterface.php
index 8fae58efdeb20bf5a3fcdbeafa9aaba744d652c9..f210611aae960119a97ddb693333be25ea75d6dd 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityFormInterface.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityFormInterface.php
@@ -36,6 +36,8 @@ public function getFormDisplay(FormStateInterface $form_state);
    *   The form display that the current form operates with.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The current state of the form.
+   *
+   * @return $this
    */
   public function setFormDisplay(EntityFormDisplayInterface $form_display, FormStateInterface $form_state);
 
@@ -61,4 +63,21 @@ public function getFormLangcode(FormStateInterface $form_state);
    */
   public function isDefaultFormLangcode(FormStateInterface $form_state);
 
+  /**
+   * {@inheritdoc}
+   *
+   * Note that extending classes should not override this method to add entity
+   * validation logic, but define further validation constraints using the
+   * entity validation API and/or provide a new validation constraint if
+   * necessary. This is the only way to ensure that the validation logic
+   * is correctly applied independently of form submissions; e.g., for REST
+   * requests.
+   * For more information about entity validation, see
+   * https://www.drupal.org/node/2015613.
+   *
+   * @return \Drupal\Core\Entity\ContentEntityTypeInterface
+   *   The built entity.
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state);
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityConfirmFormBase.php b/core/lib/Drupal/Core/Entity/EntityConfirmFormBase.php
index a11e27f0a71070b3b047e2f513292b597a05b3e3..7b320e1682578f335e03454c3e76a193fc70ab90 100644
--- a/core/lib/Drupal/Core/Entity/EntityConfirmFormBase.php
+++ b/core/lib/Drupal/Core/Entity/EntityConfirmFormBase.php
@@ -81,9 +81,6 @@ protected function actions(array $form, FormStateInterface $form_state) {
       'submit' => array(
         '#type' => 'submit',
         '#value' => $this->getConfirmText(),
-        '#validate' => array(
-          array($this, 'validate'),
-        ),
         '#submit' => array(
           array($this, 'submitForm'),
         ),
diff --git a/core/lib/Drupal/Core/Entity/EntityForm.php b/core/lib/Drupal/Core/Entity/EntityForm.php
index 6d61d0c6025b875e43b2ed0284ac1434fb671d51..e900309b3d413ebf40eea20d7afffd9c9a7ce4ca 100644
--- a/core/lib/Drupal/Core/Entity/EntityForm.php
+++ b/core/lib/Drupal/Core/Entity/EntityForm.php
@@ -219,7 +219,6 @@ protected function actions(array $form, FormStateInterface $form_state) {
     $actions['submit'] = array(
       '#type' => 'submit',
       '#value' => $this->t('Save'),
-      '#validate' => array('::validate'),
       '#submit' => array('::submitForm', '::save'),
     );
 
@@ -244,16 +243,6 @@ protected function actions(array $form, FormStateInterface $form_state) {
     return $actions;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function validate(array $form, FormStateInterface $form_state) {
-    // @todo Remove this.
-    // Execute legacy global validation handlers.
-    $form_state->setValidateHandlers([]);
-    \Drupal::service('form_validator')->executeValidateHandlers($form, $form_state);
-  }
-
   /**
    * {@inheritdoc}
    *
diff --git a/core/lib/Drupal/Core/Entity/EntityFormInterface.php b/core/lib/Drupal/Core/Entity/EntityFormInterface.php
index 26ce9098ab5f6cf35419f2fbf4858c7bc4c72be4..8ae1a9142b9826e97d177ff3a0cdb305432cf24d 100644
--- a/core/lib/Drupal/Core/Entity/EntityFormInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityFormInterface.php
@@ -94,19 +94,6 @@ public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entit
    */
   public function buildEntity(array $form, FormStateInterface $form_state);
 
-  /**
-   * Validates the submitted form values of the entity form.
-   *
-   * @param array $form
-   *   A nested array form elements comprising the form.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The current state of the form.
-   *
-   * @return \Drupal\Core\Entity\ContentEntityTypeInterface
-   *   The built entity.
-   */
-  public function validate(array $form, FormStateInterface $form_state);
-
   /**
    * Form submission handler for the 'save' action.
    *
diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php
index b4a19602671df1a7b494059ab5ab9f1635454be5..2e5cce76fb678b9f40521d42a56afcaf85cf51ac 100644
--- a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php
@@ -212,4 +212,22 @@ public function onChange($field_name);
    */
   public function validate();
 
+  /**
+   * Checks whether entity validation is required before saving the entity.
+   *
+   * @return bool
+   *   TRUE if validation is required, FALSE if not.
+   */
+  public function isValidationRequired();
+
+  /**
+   * Sets whether entity validation is required before saving the entity.
+   *
+   * @param bool $required
+   *   TRUE if validation is required, FALSE otherwise.
+   *
+   * @return $this
+   */
+  public function setValidationRequired($required);
+
 }
diff --git a/core/modules/action/src/ActionFormBase.php b/core/modules/action/src/ActionFormBase.php
index bdddc9818e6742939a5a20a28cbe997b422e2c02..0b972d493ed59e1b1423b355d6b7c9bfe96b450b 100644
--- a/core/modules/action/src/ActionFormBase.php
+++ b/core/modules/action/src/ActionFormBase.php
@@ -123,8 +123,8 @@ protected function actions(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     if ($this->plugin instanceof PluginFormInterface) {
       $this->plugin->validateConfigurationForm($form, $form_state);
diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php
index ac9406a8feb5302411299d041f42a2c095c48002..07da65b5770c2bb0dd648d4d3d9bf9da07a97c1d 100644
--- a/core/modules/block/src/BlockForm.php
+++ b/core/modules/block/src/BlockForm.php
@@ -273,8 +273,8 @@ protected function actions(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     // The Block Entity form puts all block plugin form elements in the
     // settings form element, so just pass that to the block for validation.
diff --git a/core/modules/block_content/src/BlockContentForm.php b/core/modules/block_content/src/BlockContentForm.php
index 379e5763560e06728b2a03e446f2b43dd7cec7de..ec267cc39311112f4e00c9bc854490e72ebadaf9 100644
--- a/core/modules/block_content/src/BlockContentForm.php
+++ b/core/modules/block_content/src/BlockContentForm.php
@@ -224,7 +224,8 @@ public function save(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function validateForm(array &$form, FormStateInterface $form_state) {
-    if ($this->entity->isNew()) {
+    $entity = parent::validateForm($form, $form_state);
+    if ($entity->isNew()) {
       $exists = $this->blockContentStorage->loadByProperties(array('info' => $form_state->getValue(['info', 0, 'value'])));
       if (!empty($exists)) {
         $form_state->setErrorByName('info', $this->t('A block with description %name already exists.', array(
@@ -232,6 +233,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
         )));
       }
     }
+    return $entity;
   }
 
 }
diff --git a/core/modules/comment/src/CommentForm.php b/core/modules/comment/src/CommentForm.php
index 0bcacee9eda2d6b91cf238d5242374bdff031d75..e786fe1c77a01877bcecc32d7b0593b56ed11976 100644
--- a/core/modules/comment/src/CommentForm.php
+++ b/core/modules/comment/src/CommentForm.php
@@ -258,7 +258,6 @@ protected function actions(array $form, FormStateInterface $form_state) {
       '#type' => 'submit',
       '#value' => $this->t('Preview'),
       '#access' => $preview_mode != DRUPAL_DISABLED,
-      '#validate' => array('::validate'),
       '#submit' => array('::submitForm', '::preview'),
     );
 
diff --git a/core/modules/contact/src/ContactFormEditForm.php b/core/modules/contact/src/ContactFormEditForm.php
index 3300c593b69c5509675dea0a23e784357ffa5658..c545b0e53abec012c0e0877cd3613a334b8e6880 100644
--- a/core/modules/contact/src/ContactFormEditForm.php
+++ b/core/modules/contact/src/ContactFormEditForm.php
@@ -112,8 +112,8 @@ public function form(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     // Validate and each email recipient.
     $recipients = explode(',', $form_state->getValue('recipients'));
diff --git a/core/modules/contact/src/MessageForm.php b/core/modules/contact/src/MessageForm.php
index 78de6ddc596a066992a3a92c326f152121e7d866..27352c7a69d713141f135b0073e27b2b26fbd1b3 100644
--- a/core/modules/contact/src/MessageForm.php
+++ b/core/modules/contact/src/MessageForm.php
@@ -168,8 +168,8 @@ public function actions(array $form, FormStateInterface $form_state) {
     $elements = parent::actions($form, $form_state);
     $elements['submit']['#value'] = $this->t('Send message');
     $elements['preview'] = array(
+      '#type' => 'submit',
       '#value' => $this->t('Preview'),
-      '#validate' => array('::validate'),
       '#submit' => array('::submitForm', '::preview'),
     );
     return $elements;
@@ -187,10 +187,8 @@ public function preview(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
-
-    $message = $this->entity;
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $message = parent::validateForm($form, $form_state);
 
     // Check if flood control has been activated for sending emails.
     if (!$this->currentUser()->hasPermission('administer contact forms') && (!$message->isPersonal() || !$this->currentUser()->hasPermission('administer users'))) {
@@ -204,6 +202,8 @@ public function validate(array $form, FormStateInterface $form_state) {
         )));
       }
     }
+
+    return $message;
   }
 
   /**
diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php
index 28529eb0b29023d3503df3c81675af9af0bf042d..10cf5f3b84a99596379a2fdfff2824d0145c227f 100644
--- a/core/modules/content_translation/src/ContentTranslationHandler.php
+++ b/core/modules/content_translation/src/ContentTranslationHandler.php
@@ -473,9 +473,7 @@ public function entityFormAlter(array &$form, FormStateInterface $form_state, En
     $form['#entity_builders'][] = array($this, 'entityFormEntityBuild');
 
     // Handle entity validation.
-    if (isset($form['actions']['submit'])) {
-      $form['actions']['submit']['#validate'][] = array($this, 'entityFormValidate');
-    }
+    $form['#validate'][] = array($this, 'entityFormValidate');
 
     // Handle entity deletion.
     if (isset($form['actions']['delete'])) {
diff --git a/core/modules/field_ui/src/Form/EntityDisplayModeAddForm.php b/core/modules/field_ui/src/Form/EntityDisplayModeAddForm.php
index d6d801b7551c41bd59397fac5de391a6695933b5..350a2e99badba11261e27560d1577cecc0b6c9ab 100644
--- a/core/modules/field_ui/src/Form/EntityDisplayModeAddForm.php
+++ b/core/modules/field_ui/src/Form/EntityDisplayModeAddForm.php
@@ -38,8 +38,8 @@ public function buildForm(array $form, FormStateInterface $form_state, $entity_t
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     $form_state->setValueForElement($form['id'], $this->targetEntityTypeId . '.' . $form_state->getValue('id'));
   }
diff --git a/core/modules/field_ui/src/Form/FieldConfigEditForm.php b/core/modules/field_ui/src/Form/FieldConfigEditForm.php
index 866ff9a48ab6e3625740313e51d75a73912a6715..52ace9581f780bc937e183c904267aa53774a2ae 100644
--- a/core/modules/field_ui/src/Form/FieldConfigEditForm.php
+++ b/core/modules/field_ui/src/Form/FieldConfigEditForm.php
@@ -150,8 +150,8 @@ protected function actions(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     if (isset($form['default_value'])) {
       $item = $form['#entity']->get($this->entity->getName());
diff --git a/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php b/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php
index 58c4ea9030d856b71874a3ad602e74f0802cba8c..adafcc79000b21a0a9a9c702032136c4f0a18b93 100644
--- a/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php
+++ b/core/modules/field_ui/src/Form/FieldStorageConfigEditForm.php
@@ -146,8 +146,8 @@ protected function actions(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     // Validate field cardinality.
     if ($form_state->getValue('cardinality') === 'number' && !$form_state->getValue('cardinality_number')) {
diff --git a/core/modules/filter/src/FilterFormatFormBase.php b/core/modules/filter/src/FilterFormatFormBase.php
index ff140c3b0bc114c37b53076775eb674c6a5b1d25..77b5433598432a71080f1ff7fca99190b777fa30 100644
--- a/core/modules/filter/src/FilterFormatFormBase.php
+++ b/core/modules/filter/src/FilterFormatFormBase.php
@@ -204,8 +204,8 @@ public function exists($format_id) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     // @todo Move trimming upstream.
     $format_format = trim($form_state->getValue('format'));
diff --git a/core/modules/forum/src/Form/Overview.php b/core/modules/forum/src/Form/Overview.php
index 24f5142aa62a06d3e363650a973ccc999f20af7b..dae7fb54c0b525201b8ac49e1b3899b0a11b99d0 100644
--- a/core/modules/forum/src/Form/Overview.php
+++ b/core/modules/forum/src/Form/Overview.php
@@ -86,9 +86,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     // Remove the alphabetical reset.
     unset($form['actions']['reset_alphabetical']);
 
-    // The form needs to have submit and validate handlers set explicitly.
     // Use the existing taxonomy overview submit handler.
-    $form['#submit'] = array('::submitForm');
     $form['terms']['#empty'] = $this->t('No containers or forums available. <a href="@container">Add container</a> or <a href="@forum">Add forum</a>.', array(
       '@container' => $this->url('forum.add_container'),
       '@forum' => $this->url('forum.add_forum')
diff --git a/core/modules/menu_ui/menu_ui.module b/core/modules/menu_ui/menu_ui.module
index 0c097ba363ff4d97ce0739b53646e348b010de7a..f26dade62d1c85e5475c899146cacc790a2d0678 100644
--- a/core/modules/menu_ui/menu_ui.module
+++ b/core/modules/menu_ui/menu_ui.module
@@ -415,7 +415,7 @@ function menu_ui_form_node_type_form_alter(&$form, FormStateInterface $form_stat
   );
   $options_cacheability->applyTo($form['menu']['menu_parent']);
 
-  $form['actions']['submit']['#validate'][] = 'menu_ui_form_node_type_form_validate';
+  $form['#validate'][] = 'menu_ui_form_node_type_form_validate';
   $form['#entity_builders'][] = 'menu_ui_form_node_type_form_builder';
 }
 
diff --git a/core/modules/node/src/NodeForm.php b/core/modules/node/src/NodeForm.php
index c5846606831da4ba23fb730eba94476f51752874..e363dca9923d3a972e5e432465daa859079e062b 100644
--- a/core/modules/node/src/NodeForm.php
+++ b/core/modules/node/src/NodeForm.php
@@ -297,7 +297,6 @@ protected function actions(array $form, FormStateInterface $form_state) {
       '#access' => $preview_mode != DRUPAL_DISABLED && ($node->access('create') || $node->access('update')),
       '#value' => t('Preview'),
       '#weight' => 20,
-      '#validate' => array('::validate'),
       '#submit' => array('::submitForm', '::preview'),
     );
 
diff --git a/core/modules/node/src/NodeTypeForm.php b/core/modules/node/src/NodeTypeForm.php
index 6dfc669df7cb345cc59278aefa9e77f17190f8b0..ff757f98b37921cb54fd5a1fce081a4349dd2040 100644
--- a/core/modules/node/src/NodeTypeForm.php
+++ b/core/modules/node/src/NodeTypeForm.php
@@ -204,8 +204,8 @@ protected function actions(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     $id = trim($form_state->getValue('type'));
     // '0' is invalid, since elsewhere we check it using empty().
diff --git a/core/modules/responsive_image/src/ResponsiveImageStyleForm.php b/core/modules/responsive_image/src/ResponsiveImageStyleForm.php
index a22742ae74b73062592bfd9d82f7052c56f752a0..5763bc85f7392287e8eeccfb15d37b540d4cd39a 100644
--- a/core/modules/responsive_image/src/ResponsiveImageStyleForm.php
+++ b/core/modules/responsive_image/src/ResponsiveImageStyleForm.php
@@ -142,7 +142,8 @@ public function form(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
     // Only validate on edit.
     if ($form_state->hasValue('keyed_styles')) {
       // Check if another breakpoint group is selected.
diff --git a/core/modules/search/src/Form/SearchPageFormBase.php b/core/modules/search/src/Form/SearchPageFormBase.php
index 95afb4ba962d8686934edc724a68d8aa4ea557a3..c41fd5be585647e82217616e3b14f56737e662a8 100644
--- a/core/modules/search/src/Form/SearchPageFormBase.php
+++ b/core/modules/search/src/Form/SearchPageFormBase.php
@@ -144,8 +144,8 @@ public function exists($id) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     // Ensure each path is unique.
     $path = $this->entityQuery->get('search_page')
diff --git a/core/modules/system/src/Form/DateFormatFormBase.php b/core/modules/system/src/Form/DateFormatFormBase.php
index aeaa12a7b740b8e4b7287e3eb7e4a6dea3271851..b8d0e178825264d064db7343f5efdf8fe81408fd 100644
--- a/core/modules/system/src/Form/DateFormatFormBase.php
+++ b/core/modules/system/src/Form/DateFormatFormBase.php
@@ -126,8 +126,8 @@ public function form(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     // The machine name field should already check to see if the requested
     // machine name is available. Regardless of machine_name or human readable
diff --git a/core/modules/system/src/Tests/Entity/EntityFormTest.php b/core/modules/system/src/Tests/Entity/EntityFormTest.php
index f268dd751124b0c8493fb82b9a19bd2b96a7d708..ffa5cca84ac37da4e46e346f4ae723e753fc45dd 100644
--- a/core/modules/system/src/Tests/Entity/EntityFormTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityFormTest.php
@@ -149,4 +149,25 @@ protected function loadEntityByName($entity_type, $name) {
     $entities = $entity_storage->loadByProperties(array('name' => $name));
     return $entities ? current($entities) : NULL;
   }
+
+  /**
+   * Checks that validation handlers works as expected.
+   */
+  public function testValidationHandlers() {
+    /** @var \Drupal\Core\State\StateInterface $state */
+    $state = $this->container->get('state');
+
+    // Check that from-level validation handlers can be defined and can alter
+    // the form array.
+    $state->set('entity_test.form.validate.test', 'form-level');
+    $this->drupalPostForm('entity_test/add', [], 'Save');
+    $this->assertTrue($state->get('entity_test.form.validate.result'), 'Form-level validation handlers behave correctly.');
+
+    // Check that defining a button-level validation handler causes an exception
+    // to be thrown.
+    $state->set('entity_test.form.validate.test', 'button-level');
+    $this->drupalPostForm('entity_test/add', [], 'Save');
+    $this->assertEqual($state->get('entity_test.form.save.exception'), 'Drupal\Core\Entity\EntityStorageException: Entity validation was skipped.', 'Button-level validation handlers behave correctly.');
+  }
+
 }
diff --git a/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php b/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php
index 426ac5b4398567724808245b5717d969261d95be..4f223e3533977da90dea9e3e79c21204c237dd3f 100644
--- a/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php
+++ b/core/modules/system/src/Tests/Entity/FieldWidgetConstraintValidatorTest.php
@@ -89,8 +89,12 @@ protected function getErrorsForEntity(EntityInterface $entity, $hidden_fields =
     \Drupal::formBuilder()->processForm('field_test_entity_form', $form, $form_state);
 
     // Validate the field constraint.
-    $form_state->getFormObject()->setEntity($entity)->setFormDisplay($display, $form_state);
-    $form_state->getFormObject()->validate($form, $form_state);
+    /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
+    $form_object = $form_state->getFormObject();
+    $form_object
+      ->setEntity($entity)
+      ->setFormDisplay($display, $form_state)
+      ->validateForm($form, $form_state);
 
     return $form_state->getErrors();
   }
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index 28f1e993d7784b5c367574ee3c9a5f5f1bcf3de8..1e7fa2b1f9e6156086f3aadd08b4dc36b8effa06 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -275,6 +275,37 @@ function entity_test_entity_extra_field_info() {
   return $extra;
 }
 
+/**
+ * Implements hook_form_BASE_FORM_ID_alter().
+ */
+function entity_test_form_entity_test_form_alter(&$form) {
+  switch (\Drupal::state()->get('entity_test.form.validate.test')) {
+    case 'form-level':
+      $form['#validate'][] = 'entity_test_form_entity_test_form_validate';
+      $form['#validate'][] = 'entity_test_form_entity_test_form_validate_check';
+      break;
+
+    case 'button-level':
+      $form['actions']['submit']['#validate'][] = 'entity_test_form_entity_test_form_validate';
+  }
+}
+
+/**
+ * Validation handler for the entity_test entity form.
+ */
+function entity_test_form_entity_test_form_validate(array &$form, FormStateInterface $form_state) {
+  $form['#entity_test_form_validate'] = TRUE;
+}
+
+/**
+ * Validation handler for the entity_test entity form.
+ */
+function entity_test_form_entity_test_form_validate_check(array &$form, FormStateInterface $form_state) {
+  if (!empty($form['#entity_test_form_validate'])) {
+    \Drupal::state()->set('entity_test.form.validate.result', TRUE);
+  }
+}
+
 /**
  * Implements hook_form_BASE_FORM_ID_alter().
  */
diff --git a/core/modules/system/tests/modules/entity_test/src/EntityTestForm.php b/core/modules/system/tests/modules/entity_test/src/EntityTestForm.php
index fb3c486dad943f02d161a07333bb844e8bad5e85..59e798b5b88539ba132d69637786b9613bd48064 100644
--- a/core/modules/system/tests/modules/entity_test/src/EntityTestForm.php
+++ b/core/modules/system/tests/modules/entity_test/src/EntityTestForm.php
@@ -51,35 +51,40 @@ public function form(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function save(array $form, FormStateInterface $form_state) {
-    $entity = $this->entity;
+    try {
+      $entity = $this->entity;
 
-    // Save as a new revision if requested to do so.
-    if (!$form_state->isValueEmpty('revision')) {
-      $entity->setNewRevision();
-    }
+      // Save as a new revision if requested to do so.
+      if (!$form_state->isValueEmpty('revision')) {
+        $entity->setNewRevision();
+      }
 
-    $is_new = $entity->isNew();
-    $entity->save();
+      $is_new = $entity->isNew();
+      $entity->save();
 
-    if ($is_new) {
-     $message = t('%entity_type @id has been created.', array('@id' => $entity->id(), '%entity_type' => $entity->getEntityTypeId()));
-    }
-    else {
-      $message = t('%entity_type @id has been updated.', array('@id' => $entity->id(), '%entity_type' => $entity->getEntityTypeId()));
-    }
-    drupal_set_message($message);
+      if ($is_new) {
+        $message = t('%entity_type @id has been created.', array('@id' => $entity->id(), '%entity_type' => $entity->getEntityTypeId()));
+      }
+      else {
+        $message = t('%entity_type @id has been updated.', array('@id' => $entity->id(), '%entity_type' => $entity->getEntityTypeId()));
+      }
+      drupal_set_message($message);
 
-    if ($entity->id()) {
-      $entity_type = $entity->getEntityTypeId();
-      $form_state->setRedirect(
-        "entity.$entity_type.edit_form",
-        array($entity_type => $entity->id())
-      );
+      if ($entity->id()) {
+        $entity_type = $entity->getEntityTypeId();
+        $form_state->setRedirect(
+          "entity.$entity_type.edit_form",
+          array($entity_type => $entity->id())
+        );
+      }
+      else {
+        // Error on save.
+        drupal_set_message(t('The entity could not be saved.'), 'error');
+        $form_state->setRebuild();
+      }
     }
-    else {
-      // Error on save.
-      drupal_set_message(t('The entity could not be saved.'), 'error');
-      $form_state->setRebuild();
+    catch (\Exception $e) {
+      \Drupal::state()->set('entity_test.form.save.exception', get_class($e) . ': ' . $e->getMessage());
     }
   }
 
diff --git a/core/modules/taxonomy/src/TermForm.php b/core/modules/taxonomy/src/TermForm.php
index a0d1279f0d61ba0b1067250a6c9a02314a936512..9da0993643d3572ba49258abbf167b19a14b8c58 100644
--- a/core/modules/taxonomy/src/TermForm.php
+++ b/core/modules/taxonomy/src/TermForm.php
@@ -96,8 +96,8 @@ public function form(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     // Ensure numeric values.
     if ($form_state->hasValue('weight') && !is_numeric($form_state->getValue('weight'))) {
diff --git a/core/modules/views_ui/src/ViewAddForm.php b/core/modules/views_ui/src/ViewAddForm.php
index 6db3e95e6cdfdfeafd1ff2c2e9db659ef83e4c57..5b816d93cb733dcc1540c70b6fbbfe8c63f05b1d 100644
--- a/core/modules/views_ui/src/ViewAddForm.php
+++ b/core/modules/views_ui/src/ViewAddForm.php
@@ -162,7 +162,7 @@ protected function actions(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
+  public function validateForm(array &$form, FormStateInterface $form_state) {
     $wizard_type = $form_state->getValue(array('show', 'wizard_key'));
     $wizard_instance = $this->wizardManager->createInstance($wizard_type);
     $form_state->set('wizard', $wizard_instance->getPluginDefinition());
diff --git a/core/modules/views_ui/src/ViewDuplicateForm.php b/core/modules/views_ui/src/ViewDuplicateForm.php
index 36f1a7fc5463597d70e95be2c9a2eb0874ac5697..ccc1a49f5517064a2b6606426522c35a1b33065b 100644
--- a/core/modules/views_ui/src/ViewDuplicateForm.php
+++ b/core/modules/views_ui/src/ViewDuplicateForm.php
@@ -58,7 +58,6 @@ protected function actions(array $form, FormStateInterface $form_state) {
     $actions['submit'] = array(
       '#type' => 'submit',
       '#value' => $this->t('Duplicate'),
-      '#submit' => array('::submitForm'),
     );
     return $actions;
   }
diff --git a/core/modules/views_ui/src/ViewEditForm.php b/core/modules/views_ui/src/ViewEditForm.php
index 9e598f28d94e5ce53e8e2283702faf159f1ad6b3..b11f54afe2d89adaeff1802d2afa1abcc9fb10aa 100644
--- a/core/modules/views_ui/src/ViewEditForm.php
+++ b/core/modules/views_ui/src/ViewEditForm.php
@@ -264,8 +264,8 @@ protected function actions(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validate(array $form, FormStateInterface $form_state) {
-    parent::validate($form, $form_state);
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
 
     $view = $this->entity;
     if ($view->isLocked()) {
diff --git a/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php b/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php
index b3bd6bd784c09f223aeb7bd941433f4206d34ad7..f00ecfa401102d9dd55106fc164ad7ff5f02b55b 100644
--- a/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/ContentEntityBaseUnitTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\Tests\UnitTestCase;
@@ -349,6 +350,64 @@ public function testValidate() {
     $this->assertSame(1, count($this->entity->validate()));
   }
 
+  /**
+   * Tests required validation.
+   *
+   * @covers ::validate
+   * @covers ::isValidationRequired
+   * @covers ::setValidationRequired
+   * @covers ::save
+   * @covers ::preSave
+   *
+   * @expectedException \LogicException
+   * @expectedExceptionMessage Entity validation was skipped.
+   */
+  public function testRequiredValidation() {
+    $validator = $this->getMock('\Symfony\Component\Validator\ValidatorInterface');
+    /** @var \Symfony\Component\Validator\ConstraintViolationList|\PHPUnit_Framework_MockObject_MockObject $empty_violation_list */
+    $empty_violation_list = $this->getMockBuilder('\Symfony\Component\Validator\ConstraintViolationList')
+      ->setMethods(NULL)
+      ->getMock();
+    $validator->expects($this->at(0))
+      ->method('validate')
+      ->with($this->entity->getTypedData())
+      ->will($this->returnValue($empty_violation_list));
+    $this->typedDataManager->expects($this->any())
+      ->method('getValidator')
+      ->will($this->returnValue($validator));
+
+    /** @var \Drupal\Core\Entity\EntityStorageInterface|\PHPUnit_Framework_MockObject_MockObject $storage */
+    $storage = $this->getMock('\Drupal\Core\Entity\EntityStorageInterface');
+    $storage->expects($this->any())
+      ->method('save')
+      ->willReturnCallback(function (ContentEntityInterface $entity) use ($storage) {
+        $entity->preSave($storage);
+      });
+
+    $this->entityManager->expects($this->any())
+      ->method('getStorage')
+      ->with($this->entityTypeId)
+      ->will($this->returnValue($storage));
+
+    // Check that entities can be saved normally when validation is not
+    // required.
+    $this->assertFalse($this->entity->isValidationRequired());
+    $this->entity->save();
+
+    // Make validation required and check that if the entity is validated, it
+    // can be saved normally.
+    $this->entity->setValidationRequired(TRUE);
+    $this->assertTrue($this->entity->isValidationRequired());
+    $this->entity->validate();
+    $this->entity->save();
+
+    // Check that the "validated" status is reset after saving the entity and
+    // that trying to save a non-validated entity when validation is required
+    // results in an exception.
+    $this->assertTrue($this->entity->isValidationRequired());
+    $this->entity->save();
+  }
+
   /**
    * @covers ::bundle
    */