diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
index cd7ad17..9e53110 100644
--- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php
+++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
@@ -13,11 +13,12 @@
 use Drupal\Core\DependencyInjection\ClassResolverInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\TypedData\Validation\ExecutionContextFactory;
 use Drupal\Core\TypedData\Validation\MetadataFactory;
+use Drupal\Core\TypedData\Validation\RecursiveValidator;
 use Drupal\Core\Validation\ConstraintManager;
 use Drupal\Core\Validation\ConstraintValidatorFactory;
 use Drupal\Core\Validation\DrupalTranslator;
-use Symfony\Component\Validator\Validation;
 use Symfony\Component\Validator\Validator\ValidatorInterface;
 
 /**
@@ -28,7 +29,7 @@ class TypedDataManager extends DefaultPluginManager {
   /**
    * The validator used for validating typed data.
    *
-   * @var \Symfony\Component\Validator\ValidatorInterface
+   * @var \Symfony\Component\Validator\Validator\ValidatorInterface
    */
   protected $validator;
 
@@ -331,12 +332,11 @@ public function setValidator(ValidatorInterface $validator) {
    */
   public function getValidator() {
     if (!isset($this->validator)) {
-      $this->validator = Validation::createValidatorBuilder()
-        ->setMetadataFactory(new MetadataFactory($this))
-        ->setTranslator(new DrupalTranslator())
-        ->setConstraintValidatorFactory(new ConstraintValidatorFactory($this->classResolver))
-        ->setApiVersion(Validation::API_VERSION_2_4)
-        ->getValidator();
+      $this->validator = new RecursiveValidator(
+        new ExecutionContextFactory(new DrupalTranslator()),
+        new ConstraintValidatorFactory($this->classResolver),
+        $this
+      );
     }
     return $this->validator;
   }
diff --git a/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php b/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php
new file mode 100644
index 0000000..efe7192
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php
@@ -0,0 +1,181 @@
+<?php
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Symfony\Component\Translation\TranslatorInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintViolation;
+use Symfony\Component\Validator\ConstraintViolationList;
+use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface;
+
+/**
+ * Defines a constraint violation builder for the Typed Data validator.
+ *
+ * We do not use the builder provided by Symfony as it is marked internal.
+ */
+class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface {
+
+  /**
+   * @var ConstraintViolationList
+   */
+  protected $violations;
+
+  /**
+   * @var string
+   */
+  protected $message;
+
+  /**
+   * @var array
+   */
+  protected $parameters;
+
+  /**
+   * @var mixed
+   */
+  protected $root;
+
+  /**
+   * @var mixed
+   */
+  protected $invalidValue;
+
+  /**
+   * @var string
+   */
+  protected $propertyPath;
+
+  /**
+   * @var TranslatorInterface
+   */
+  protected $translator;
+
+  /**
+   * @var string|null
+   */
+  protected $translationDomain;
+
+  /**
+   * @var int|null
+   */
+  protected $plural;
+
+  /**
+   * @var Constraint
+   */
+  protected $constraint;
+
+  /**
+   * @var mixed
+   */
+  protected $code;
+
+  /**
+   * @var mixed
+   */
+  protected $cause;
+
+  public function __construct(ConstraintViolationList $violations, Constraint $constraint, $message, array $parameters, $root, $propertyPath, $invalidValue, TranslatorInterface $translator, $translationDomain = NULL) {
+    $this->violations = $violations;
+    $this->message = $message;
+    $this->parameters = $parameters;
+    $this->root = $root;
+    $this->propertyPath = $propertyPath;
+    $this->invalidValue = $invalidValue;
+    $this->translator = $translator;
+    $this->translationDomain = $translationDomain;
+    $this->constraint = $constraint;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function atPath($path) {
+    $this->propertyPath = PropertyPath::append($this->propertyPath, $path);
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setParameter($key, $value) {
+    $this->parameters[$key] = $value;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setParameters(array $parameters) {
+    $this->parameters = $parameters;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setTranslationDomain($translationDomain) {
+    $this->translationDomain = $translationDomain;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setInvalidValue($invalidValue) {
+    $this->invalidValue = $invalidValue;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setPlural($number) {
+    $this->plural = $number;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCode($code) {
+    $this->code = $code;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setCause($cause) {
+    $this->cause = $cause;
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addViolation() {
+    if (NULL === $this->plural) {
+      $translatedMessage = $this->translator->trans($this->message, $this->parameters, $this->translationDomain);
+    }
+    else {
+      try {
+        $translatedMessage = $this->translator->transChoice($this->message, $this->plural, $this->parameters, $this->translationDomain#
+        );
+      }
+      catch (\InvalidArgumentException $e) {
+        $translatedMessage = $this->translator->trans($this->message, $this->parameters, $this->translationDomain);
+      }
+    }
+
+    $this->violations->add(new ConstraintViolation($translatedMessage, $this->message, $this->parameters, $this->root, $this->propertyPath, $this->invalidValue, $this->plural, $this->code, $this->constraint, $this->cause));
+  }
+}
diff --git a/core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php
new file mode 100644
index 0000000..066ed0a
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php
@@ -0,0 +1,317 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\ExecutionContext.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Symfony\Component\Translation\TranslatorInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintViolation;
+use Symfony\Component\Validator\ConstraintViolationList;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
+use Symfony\Component\Validator\Mapping\MetadataInterface;
+use Symfony\Component\Validator\Validator\ValidatorInterface;
+
+/**
+ * Defines an execution context class.
+ *
+ * We do not use the context provided by Symfony as it is marked internal.
+ */
+class ExecutionContext implements ExecutionContextInterface {
+
+  /**
+   * @var ValidatorInterface
+   */
+  protected $validator;
+
+  /**
+   * The root value of the validated object graph.
+   *
+   * @var mixed
+   */
+  protected $root;
+
+  /**
+   * @var TranslatorInterface
+   */
+  protected $translator;
+
+  /**
+   * @var string
+   */
+  protected $translationDomain;
+
+  /**
+   * The violations generated in the current context.
+   *
+   * @var ConstraintViolationList
+   */
+  protected $violations;
+
+  /**
+   * The currently validated value.
+   *
+   * @var mixed
+   */
+  protected $value;
+
+  /**
+   * The currently validated typed data object.
+   *
+   * @var \Drupal\Core\TypedData\TypedDataInterface
+   */
+  protected $data;
+
+  /**
+   * The property path leading to the current value.
+   *
+   * @var string
+   */
+  protected $propertyPath = '';
+
+  /**
+   * The current validation metadata.
+   *
+   * @var MetadataInterface|null
+   */
+  protected $metadata;
+
+  /**
+   * The currently validated group.
+   *
+   * @var string|null
+   */
+  protected $group;
+
+  /**
+   * The currently validated constraint.
+   *
+   * @var Constraint|null
+   */
+  protected $constraint;
+
+  /**
+   * Stores which objects have been validated in which group.
+   *
+   * @var array
+   */
+  protected $validatedObjects = array();
+
+  /**
+   * Stores which class constraint has been validated for which object.
+   *
+   * @var array
+   */
+  protected $validatedConstraints = array();
+
+  /**
+   * Creates a new execution context.
+   *
+   * @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator
+   *   The validator.
+   * @param mixed $root The root value of the
+   *                                               validated object graph
+   * @param TranslatorInterface $translator The translator
+   * @param string|null $translationDomain The translation domain to
+   *                                               use for translating
+   *                                               violation messages
+   *
+   * @internal Called by {@link ExecutionContextFactory}. Should not be used
+   *           in user code.
+   */
+  public function __construct(ValidatorInterface $validator, $root, TranslatorInterface $translator, $translationDomain = NULL) {
+    $this->validator = $validator;
+    $this->root = $root;
+    $this->translator = $translator;
+    $this->translationDomain = $translationDomain;
+    $this->violations = new ConstraintViolationList();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setNode($value, $object, MetadataInterface $metadata = NULL, $propertyPath) {
+    $this->value = $value;
+    $this->data = $object;
+    $this->metadata = $metadata;
+    $this->propertyPath = (string) $propertyPath;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setGroup($group) {
+    $this->group = $group;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConstraint(Constraint $constraint) {
+    $this->constraint = $constraint;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addViolation($message, array $parameters = array(), $invalidValue = NULL, $plural = NULL, $code = NULL) {
+    // The parameters $invalidValue and following are ignored by the new
+    // API, as they are not present in the new interface anymore.
+    // You should use buildViolation() instead.
+    if (func_num_args() > 2) {
+      throw new \LogicException('Legacy validator API is unsupported.');
+    }
+
+    $this->violations->add(new ConstraintViolation($this->translator->trans($message, $parameters, $this->translationDomain), $message, $parameters, $this->root, $this->propertyPath, $this->value, NULL, NULL, $this->constraint));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildViolation($message, array $parameters = array()) {
+    return new ConstraintViolationBuilder($this->violations, $this->constraint, $message, $parameters, $this->root, $this->propertyPath, $this->value, $this->translator, $this->translationDomain);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViolations() {
+    return $this->violations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValidator() {
+    return $this->validator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoot() {
+    return $this->root;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    return $this->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getObject() {
+    return $this->data;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadata() {
+    return $this->metadata;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroup() {
+    return Constraint::DEFAULT_GROUP;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getClassName() {
+    return get_class($this->data);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyName() {
+    return $this->data->getName();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyPath($sub_path = '') {
+    return PropertyPath::append($this->propertyPath, $sub_path);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addViolationAt($subPath, $message, array $parameters = array(), $invalidValue = NULL, $plural = NULL, $code = NULL) {
+    throw new \LogicException('Legacy validator API is unsupported.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, $subPath = '', $groups = NULL, $traverse = FALSE, $deep = FALSE) {
+    throw new \LogicException('Legacy validator API is unsupported.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function markConstraintAsValidated($cache_key, $constraint_hash) {
+    $this->validatedConstraints[$cache_key . ':' . $constraint_hash] = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isConstraintValidated($cache_key, $constraint_hash) {
+    return isset($this->validatedConstraints[$cache_key . ':' . $constraint_hash]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateValue($value, $constraints, $subPath = '', $groups = NULL) {
+    throw new \LogicException('Legacy validator API is unsupported.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function markGroupAsValidated($cache_key, $group_hash) {
+    $this->validatedObjects[$cache_key][$group_hash] = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isGroupValidated($cache_key, $group_hash) {
+    return isset($this->validatedObjects[$cache_key][$group_hash]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function markObjectAsInitialized($cache_key) {
+    // Not supported, so nothing todo.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isObjectInitialized($cache_key) {
+    // Not supported, so nothing todo.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadataFactory() {
+    throw new \LogicException('Legacy validator API is unsupported.');
+  }
+}
diff --git a/core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php
new file mode 100644
index 0000000..2eda012
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\ExecutionContextFactory.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Symfony\Component\Translation\TranslatorInterface;
+use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface;
+use Symfony\Component\Validator\Validator\ValidatorInterface;
+
+/**
+ * Defines an execution factory for the Typed Data validator.
+ *
+ * We do not use the factory provided by Symfony as it is marked internal.
+ */
+class ExecutionContextFactory implements ExecutionContextFactoryInterface {
+
+  /**
+   * @var TranslatorInterface
+   */
+  protected $translator;
+
+  /**
+   * @var string|null
+   */
+  protected $translationDomain;
+
+  /**
+   * Creates a new context factory.
+   *
+   * @param TranslatorInterface $translator The translator
+   * @param string|null $translationDomain The translation domain to
+   *                                               use for translating
+   *                                               violation messages
+   */
+  public function __construct(TranslatorInterface $translator, $translationDomain = NULL) {
+    $this->translator = $translator;
+    $this->translationDomain = $translationDomain;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createContext(ValidatorInterface $validator, $root) {
+    return new ExecutionContext($validator, $root, $this->translator, $this->translationDomain);
+  }
+}
diff --git a/core/lib/Drupal/Core/TypedData/Validation/Metadata.php b/core/lib/Drupal/Core/TypedData/Validation/Metadata.php
deleted file mode 100644
index 6fe5255..0000000
--- a/core/lib/Drupal/Core/TypedData/Validation/Metadata.php
+++ /dev/null
@@ -1,115 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\TypedData\Validation\Metadata.
- */
-
-namespace Drupal\Core\TypedData\Validation;
-
-use Drupal\Core\TypedData\TypedDataInterface;
-use Drupal\Core\TypedData\TypedDataManager;
-use Symfony\Component\Validator\ValidationVisitorInterface;
-use Symfony\Component\Validator\PropertyMetadataInterface;
-
-/**
- * Typed data implementation of the validator MetadataInterface.
- */
-class Metadata implements PropertyMetadataInterface {
-
-  /**
-   * The name of the property, or empty if this is the root.
-   *
-   * @var string
-   */
-  protected $name;
-
-  /**
-   * The typed data object the metadata is about.
-   *
-   * @var \Drupal\Core\TypedData\TypedDataInterface
-   */
-  protected $typedData;
-
-  /**
-   * The metadata factory used.
-   *
-   * @var \Drupal\Core\TypedData\Validation\MetadataFactory
-   */
-  protected $factory;
-
-  /**
-   * The typed data manager.
-   *
-   * @var \Drupal\Core\TypedData\TypedDataManager
-   */
-  protected $typedDataManager;
-
-  /**
-   * Constructs the object.
-   *
-   * @param \Drupal\Core\TypedData\TypedDataInterface $typed_data
-   *   The typed data object the metadata is about.
-   * @param $name
-   *   The name of the property to get metadata for. Leave empty, if
-   *   the data is the root of the typed data tree.
-   * @param \Drupal\Core\TypedData\Validation\MetadataFactory $factory
-   *   The factory to use for instantiating property metadata.
-   * @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager
-   *   The typed data manager.
-   */
-  public function __construct(TypedDataInterface $typed_data, $name = '', MetadataFactory $factory, TypedDataManager $typed_data_manager) {
-    $this->typedData = $typed_data;
-    $this->name = $name;
-    $this->factory = $factory;
-    $this->typedDataManager = $typed_data_manager;
-  }
-
-  /**
-   * Implements MetadataInterface::accept().
-   */
-  public function accept(ValidationVisitorInterface $visitor, $typed_data, $group, $propertyPath) {
-
-    // @todo: Do we have to care about groups? Symfony class metadata has
-    // $propagatedGroup.
-
-    $visitor->visit($this, $this->typedDataManager->getCanonicalRepresentation($typed_data), $group, $propertyPath);
-  }
-
-  /**
-   * Implements MetadataInterface::findConstraints().
-   */
-  public function findConstraints($group) {
-    return $this->typedData->getConstraints();
-  }
-
-  /**
-   * Returns the name of the property.
-   *
-   * @return string The property name.
-   */
-  public function getPropertyName() {
-    return $this->name;
-  }
-
-  /**
-   * Extracts the value of the property from the given container.
-   *
-   * @param mixed $container The container to extract the property value from.
-   *
-   * @return mixed The value of the property.
-   */
-  public function getPropertyValue($container) {
-    return $this->typedDataManager->getCanonicalRepresentation($this->typedData);
-  }
-
-  /**
-   * Returns the typed data object.
-   *
-   * @return \Drupal\Core\TypedData\TypedDataInterface
-   *   The typed data object.
-   */
-  public function getTypedData() {
-    return $this->typedData;
-  }
-}
diff --git a/core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php b/core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php
deleted file mode 100644
index fcd0557..0000000
--- a/core/lib/Drupal/Core/TypedData/Validation/MetadataFactory.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\TypedData\Validation\MetadataFactory.
- */
-
-namespace Drupal\Core\TypedData\Validation;
-
-use Drupal\Core\TypedData\ComplexDataInterface;
-use Drupal\Core\TypedData\ListInterface;
-use Drupal\Core\TypedData\TypedDataInterface;
-use Drupal\Core\TypedData\TypedDataManager;
-use Symfony\Component\Validator\MetadataFactoryInterface;
-
-/**
- * Typed data implementation of the validator MetadataFactoryInterface.
- */
-class MetadataFactory implements MetadataFactoryInterface {
-
-  /**
-   * The typed data manager.
-   *
-   * @var \Drupal\Core\TypedData\TypedDataManager
-   */
-  protected $typedDataManager;
-
-  /**
-   * Constructs the object.
-   *
-   * @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager
-   *   The typed data manager.
-   */
-  public function __construct(TypedDataManager $typed_data_manager) {
-    $this->typedDataManager = $typed_data_manager;
-  }
-
-  /**
-   * {@inheritdoc}
-   *
-   * @param \Drupal\Core\TypedData\TypedDataInterface $typed_data
-   *   Some typed data object containing the value to validate.
-   * @param $name
-   *   (optional) The name of the property to get metadata for. Leave empty, if
-   *   the data is the root of the typed data tree.
-   */
-  public function getMetadataFor($typed_data, $name = '') {
-    if (!$typed_data instanceof TypedDataInterface) {
-      throw new \InvalidArgumentException('The passed value must be a typed data object.');
-    }
-    $is_container = $typed_data instanceof ComplexDataInterface || $typed_data instanceof ListInterface;
-    $class = '\Drupal\Core\TypedData\Validation\\' . ($is_container ? 'PropertyContainerMetadata' : 'Metadata');
-    return new $class($typed_data, $name, $this, $this->typedDataManager);
-  }
-
-  /**
-   * Implements MetadataFactoryInterface::hasMetadataFor().
-   */
-  public function hasMetadataFor($value) {
-    return $value instanceof TypedDataInterface;
-  }
-}
diff --git a/core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php b/core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php
deleted file mode 100644
index 80d3320..0000000
--- a/core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\Core\TypedData\Validation\PropertyContainerMetadata.
- */
-
-namespace Drupal\Core\TypedData\Validation;
-
-use Drupal\Core\TypedData\ComplexDataInterface;
-use Drupal\Core\TypedData\ListInterface;
-use Symfony\Component\Validator\PropertyMetadataContainerInterface;
-use Symfony\Component\Validator\ValidationVisitorInterface;
-
-/**
- * Typed data implementation of the validator MetadataInterface.
- */
-class PropertyContainerMetadata extends Metadata implements PropertyMetadataContainerInterface {
-
-  /**
-   * Overrides Metadata::accept().
-   */
-  public function accept(ValidationVisitorInterface $visitor, $typed_data, $group, $propertyPath) {
-    // To let all constraints properly handle empty structures, pass on NULL
-    // if the data structure is empty. That way existing NotNull or NotBlank
-    // constraints work as expected.
-    if ($typed_data->isEmpty()) {
-      $data = NULL;
-    }
-    else {
-      $data = $this->typedDataManager->getCanonicalRepresentation($typed_data);
-    }
-    $visitor->visit($this, $data, $group, $propertyPath);
-    $pathPrefix = isset($propertyPath) && $propertyPath !== '' ? $propertyPath . '.' : '';
-
-    // Only continue validating if the data is not empty.
-    if ($data) {
-      foreach ($typed_data as $name => $data) {
-        $metadata = $this->factory->getMetadataFor($data, $name);
-        $metadata->accept($visitor, $data, $group, $pathPrefix . $name);
-      }
-    }
-  }
-
-  /**
-   * Implements PropertyMetadataContainerInterface::hasPropertyMetadata().
-   */
-  public function hasPropertyMetadata($property_name) {
-    try {
-      $exists = (bool)$this->getPropertyMetadata($property_name);
-    }
-    catch (\LogicException $e) {
-      $exists = FALSE;
-    }
-    return $exists;
-  }
-
-  /**
-   * Implements PropertyMetadataContainerInterface::getPropertyMetadata().
-   */
-  public function getPropertyMetadata($property_name) {
-    if ($this->typedData instanceof ListInterface) {
-      return array(new Metadata($this->typedData[$property_name], $property_name, $this->factory, $this->typedDataManager));
-    }
-    elseif ($this->typedData instanceof ComplexDataInterface) {
-      return array(new Metadata($this->typedData->get($property_name), $property_name, $this->factory, $this->typedDataManager));
-    }
-    else {
-      throw new \LogicException("There are no known properties.");
-    }
-  }
-}
diff --git a/core/lib/Drupal/Core/TypedData/Validation/PropertyPath.php b/core/lib/Drupal/Core/TypedData/Validation/PropertyPath.php
new file mode 100644
index 0000000..e1cdb2c
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/PropertyPath.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\PropertyPath.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Symfony\Component\Validator\Util\PropertyPath as SymfonyPropertyPath;
+
+/**
+ * Bug-fixed version of Symfony's PropertyPath utility class.
+ *
+ * Works around https://github.com/symfony/symfony/issues/14394.
+ * @todo: Remove once the issue has been resolved upstream.
+ */
+class PropertyPath extends SymfonyPropertyPath {
+
+  /**
+   * @{inheritdoc}
+   */
+  public static function append($basePath, $subPath) {
+    if ('' !== (string) $subPath) {
+      if ('[' === $subPath{0}) {
+        return $basePath . $subPath;
+      }
+
+      return $basePath !== (string) '' ? $basePath . '.' . $subPath : $subPath;
+    }
+
+    return $basePath;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php
new file mode 100644
index 0000000..c42d152
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php
@@ -0,0 +1,210 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\PropertyContainerPropertyMetadata.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\ListInterface;
+use Drupal\Core\TypedData\TraversableTypedDataInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\Core\TypedData\TypedDataManager;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;;
+use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
+use Symfony\Component\Validator\Validator\ContextualValidatorInterface;
+
+/**
+ * Defines a recursive contextual validator for Typed Data.
+ */
+class RecursiveContextualValidator implements ContextualValidatorInterface {
+
+  /**
+   * @var ExecutionContextInterface
+   */
+  protected $context;
+
+  /**
+   * @var \Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface
+   */
+  protected $metadataFactory;
+
+  /**
+   * @var \Symfony\Component\Validator\ConstraintValidatorFactoryInterface
+   */
+  protected $constraintValidatorFactory;
+
+  /**
+   * Creates a validator for the given context.
+   *
+   * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
+   *   The factory for creating new contexts.
+   * @param \Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface $metadata_factory
+   *   The metadata factory.
+   * @param \Symfony\Component\Validator\ConstraintValidatorFactoryInterface $validator_factory
+   *   The constraint validator factory.
+   * @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager
+   *   The typed data manager.
+   */
+  public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $metadata_factory, ConstraintValidatorFactoryInterface $validator_factory, TypedDataManager $typed_data_manager) {
+    $this->context = $context;
+    $this->metadataFactory = $metadata_factory;
+    $this->constraintValidatorFactory = $validator_factory;
+    $this->typedDataManager = $typed_data_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function atPath($path) {
+    $this->defaultPropertyPath = $this->context->getPropertyPath($path);
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($data, $constraints = NULL, $groups = NULL, $root_call = TRUE) {
+    if (isset($groups)) {
+      throw new \LogicException('Passing custom groups is not supported.');
+    }
+
+    if (!$data instanceof TypedDataInterface) {
+      throw new \InvalidArgumentException('The passed value must be a typed data object.');
+    }
+
+    // You can pass a single constraint or an array of constraints
+    // Make sure to deal with an array in the rest of the code
+    if (isset($constraints) && !is_array($constraints)) {
+      $constraints = array($constraints);
+    }
+
+    $this->validateNode($data, $constraints, $root_call);
+    return $this;
+  }
+
+  /**
+   * Validates a Typed Data node in the validation tree.
+   *
+   * If no constraints are passed, the data is validated against the
+   * constraints specified in its data definition. If the data is complex or a
+   * list and no constraints are passed, the contained properties or list items
+   * are validated recursively.
+   *
+   * @param \Drupal\Core\TypedData\TypedDataInterface $data
+   *   The data to validated.
+   * @param \Symfony\Component\Validator\Constraint[]|null $constraints
+   *   (optional) If set, an array of constraints to validate.
+   * @param bool $root_call
+   *   (optional) Whether its the most upper call in the type data tree.
+   *
+   * @return $this
+   */
+  protected function validateNode(TypedDataInterface $data, $constraints = NULL, $root_call = FALSE) {
+    $previous_value = $this->context->getValue();
+    $previous_object = $this->context->getObject();
+    $previous_metadata = $this->context->getMetadata();
+    $previous_path = $this->context->getPropertyPath();
+
+    $metadata = $this->metadataFactory->getMetadataFor($data);
+    $cache_key = spl_object_hash($data);
+    $property_path = $root_call ? '' : PropertyPath::append($previous_path, $data->getName());
+    // Pass the canonical representation of the data as validated value to
+    // constraint validators, such that they do not have to care about Typed
+    // Data.
+    $value = $this->typedDataManager->getCanonicalRepresentation($data);
+    $this->context->setNode($value, $data, $metadata, $property_path);
+
+    if (isset($constraints) || !$this->context->isGroupValidated($cache_key, Constraint::DEFAULT_GROUP)) {
+      if (!isset($constraints)) {
+        $this->context->markGroupAsValidated($cache_key, Constraint::DEFAULT_GROUP);
+        $constraints = $metadata->findConstraints(Constraint::DEFAULT_GROUP);
+      }
+      $this->validateConstraints($value, $cache_key, $constraints);
+    }
+
+    // If the data is a list or complex data, validate the contained list items
+    // or properties. However, do not recurse if the data is empty.
+    if (($data instanceof ListInterface || $data instanceof ComplexDataInterface) && !$data->isEmpty()) {
+      foreach ($data as $name => $property) {
+        $this->validateNode($property);
+      }
+    }
+
+    $this->context->setNode($previous_value, $previous_object, $previous_metadata, $previous_path);
+
+    return $this;
+  }
+
+  /**
+   * Validates a node's value against all constraints in the given group.
+   *
+   * @param mixed $value
+   *   The validated value.
+   */
+  protected function validateConstraints($value, $cache_key, $constraints) {
+    foreach ($constraints as $constraint) {
+      // Prevent duplicate validation of constraints, in the case
+      // that constraints belong to multiple validated groups
+      if (isset($cache_key)) {
+        $constraint_hash = spl_object_hash($constraint);
+
+        if ($this->context->isConstraintValidated($cache_key, $constraint_hash)) {
+          continue;
+        }
+
+        $this->context->markConstraintAsValidated($cache_key, $constraint_hash);
+      }
+
+      $this->context->setConstraint($constraint);
+
+      $validator = $this->constraintValidatorFactory->getInstance($constraint);
+      $validator->initialize($this->context);
+      $validator->validate($value, $constraint);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViolations() {
+    return $this->context->getViolations();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateProperty($object, $propertyName, $groups = NULL) {
+    if (isset($groups)) {
+      throw new \LogicException('Passing custom groups is not supported.');
+    }
+    if (!is_object($object)) {
+      throw new \InvalidArgumentException('Passing class name is not supported.');
+    }
+    elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) {
+      throw new \InvalidArgumentException('Passed data does not contain properties.');
+    }
+    return $this->validateNode($object->get($propertyName));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validatePropertyValue($object, $property_name, $value, $groups = NULL) {
+    if (!is_object($object)) {
+      throw new \InvalidArgumentException('Passing class name is not supported.');
+    }
+    elseif (!$object instanceof ListInterface && !$object instanceof ComplexDataInterface) {
+      throw new \InvalidArgumentException('Passed data does not contain properties.');
+    }
+    $data = $object->get($property_name);
+    $constraints = $data->getDataDefinition()->getConstraints();
+    return $this->validate($value, $constraints, $groups);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php b/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php
new file mode 100644
index 0000000..723fa02
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php
@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\RecursiveValidator.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\Core\TypedData\TypedDataManager;
+use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
+use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
+use Symfony\Component\Validator\Validator\ValidatorInterface;
+
+/**
+ * Defines a recursive validator for Typed Data.
+ */
+class RecursiveValidator implements ValidatorInterface {
+
+  /**
+   * @var \Symfony\Component\Validator\Context\ExecutionContextFactoryInterface
+   */
+  protected $contextFactory;
+
+  /**
+   * @var \Symfony\Component\Validator\ConstraintValidatorFactoryInterface
+   */
+  protected $constraintValidatorFactory;
+
+  /**
+   * @var \Drupal\Core\TypedData\TypedDataManager
+   */
+  protected $typedDataManager;
+
+  /**
+   * Creates a new validator.
+   *
+   * @param \Symfony\Component\Validator\Context\ExecutionContextFactoryInterface $context_factory
+   *   The factory for creating new contexts.
+   * @param \Symfony\Component\Validator\ConstraintValidatorFactoryInterface $validator_factory
+   *   The constraint validator factory.
+   * @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager
+   *   The typed data manager.
+   */
+  public function __construct(ExecutionContextFactoryInterface $context_factory, ConstraintValidatorFactoryInterface $validator_factory, TypedDataManager $typed_data_manager) {
+    $this->contextFactory = $context_factory;
+    $this->constraintValidatorFactory = $validator_factory;
+    $this->typedDataManager = $typed_data_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function startContext($root = NULL) {
+    return new RecursiveContextualValidator($this->contextFactory->createContext($this, $root), $this, $this->constraintValidatorFactory, $this->typedDataManager);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function inContext(ExecutionContextInterface $context) {
+    return new RecursiveContextualValidator($context, $this, $this->constraintValidatorFactory, $this->typedDataManager);
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @param \Drupal\Core\TypedData\TypedDataInterface $typed_data
+   *   A typed data object containing the value to validate.
+   */
+  public function getMetadataFor($typed_data) {
+    if (!$typed_data instanceof TypedDataInterface) {
+      throw new \InvalidArgumentException('The passed value must be a typed data object.');
+    }
+    $class = '\Drupal\Core\TypedData\Validation\TypedDataMetadata';
+    return new $class($typed_data);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasMetadataFor($value) {
+    return $value instanceof TypedDataInterface;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, $constraints = null, $groups = null) {
+    return $this->startContext($value)
+      ->validate($value, $constraints, $groups)
+      ->getViolations();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateProperty($object, $propertyName, $groups = NULL) {
+    return $this->startContext($object)
+      ->validateProperty($object, $propertyName, $groups)
+      ->getViolations();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validatePropertyValue($objectOrClass, $propertyName, $value, $groups = NULL) {
+    // Just passing a class name is not supported.
+    if (!is_object($objectOrClass)) {
+      throw new \LogicException('Typed data validation does not support passing the class name only.');
+    }
+    return $this->startContext($objectOrClass)
+      ->validatePropertyValue($objectOrClass, $propertyName, $value, $groups)
+      ->getViolations();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/TypedData/Validation/TypedDataAwareValidatorTrait.php b/core/lib/Drupal/Core/TypedData/Validation/TypedDataAwareValidatorTrait.php
new file mode 100644
index 0000000..ba5beef
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/TypedDataAwareValidatorTrait.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Drupal\Core\TypedData\PrimitiveInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+
+/**
+ * Defines a trait to access the typed data object of a validated value.
+ *
+ * The trait assumes to be used on classes extending
+ * \Symfony\Component\Validator\ConstraintValidator.
+ */
+trait TypedDataAwareValidatorTrait {
+
+  /**
+   * Gets the typed data object for the validated value.
+   *
+   * @return \Drupal\Core\TypedData\TypedDataInterface
+   *   The typed data object.
+   */
+  public function getTypedData() {
+    $context = $this->context;
+    /** @var \Symfony\Component\Validator\Context\ExecutionContextInterface $context */
+    $data = $context->getObject();
+    if (!$data instanceof TypedDataInterface && !$data instanceof PrimitiveInterface) {
+      throw new \LogicException("There is no Typed Data object available.");
+    }
+    return $data;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php b/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php
new file mode 100755
index 0000000..2df09d6
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\MetadataBase.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Drupal\Core\TypedData\TypedDataInterface;
+use Symfony\Component\Validator\Exception\BadMethodCallException;
+use Symfony\Component\Validator\Mapping\CascadingStrategy;
+use Symfony\Component\Validator\Mapping\MetadataInterface;
+use Symfony\Component\Validator\Mapping\TraversalStrategy;
+use Symfony\Component\Validator\ValidationVisitorInterface;
+
+/**
+ * Validator metadata for typed data objects.
+ */
+class TypedDataMetadata implements MetadataInterface {
+
+  /**
+   * The typed data object the metadata is about.
+   *
+   * @var \Drupal\Core\TypedData\TypedDataInterface
+   */
+  protected $typedData;
+
+  /**
+   * Constructs the object.
+   *
+   * @param \Drupal\Core\TypedData\TypedDataInterface $typed_data
+   *   The typed data object the metadata is about.
+   */
+  public function __construct(TypedDataInterface $typed_data) {
+    $this->typedData = $typed_data;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function accept(ValidationVisitorInterface $visitor, $typed_data, $group, $propertyPath) {
+    throw new BadMethodCallException('Not supported.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function findConstraints($group) {
+    return $this->getConstraints();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConstraints() {
+    return $this->typedData->getConstraints();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTraversalStrategy() {
+    return TraversalStrategy::NONE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCascadingStrategy() {
+    // By default, never cascade into validating referenced data structures.
+    return CascadingStrategy::NONE;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/ConstraintManager.php b/core/lib/Drupal/Core/Validation/ConstraintManager.php
index c760d1e..0ed8d97 100644
--- a/core/lib/Drupal/Core/Validation/ConstraintManager.php
+++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php
@@ -79,14 +79,9 @@ public function create($name, $options) {
    * @see ConstraintManager::__construct()
    */
   public function registerDefinitions() {
-    $this->discovery->setDefinition('Null', array(
-      'label' => new TranslationWrapper('Null'),
-      'class' => '\Symfony\Component\Validator\Constraints\Null',
-      'type' => FALSE,
-    ));
-    $this->discovery->setDefinition('NotNull', array(
-      'label' => new TranslationWrapper('Not null'),
-      'class' => '\Symfony\Component\Validator\Constraints\NotNull',
+    $this->discovery->setDefinition('Callback', array(
+      'label' => new TranslationWrapper('Callback'),
+      'class' => '\Symfony\Component\Validator\Constraints\Callback',
       'type' => FALSE,
     ));
     $this->discovery->setDefinition('Blank', array(
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php
index efc6906..a85fbf2 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/AllowedValuesConstraintValidator.php
@@ -7,25 +7,53 @@
 
 namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
 
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\TypedData\OptionsProviderInterface;
 use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\Constraints\ChoiceValidator;
 
 /**
  * Validates the AllowedValues constraint.
  */
-class AllowedValuesConstraintValidator extends ChoiceValidator {
+class AllowedValuesConstraintValidator extends ChoiceValidator implements ContainerInjectionInterface {
+
+  use TypedDataAwareValidatorTrait;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
 
   /**
    * {@inheritdoc}
    */
-  public function validate($value, Constraint $constraint) {
-    $typed_data = $this->context->getMetadata()->getTypedData();
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('current_user'));
+  }
+
+  /**
+   * Constructs a new AllowedValuesConstraintValidator.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   */
+  public function __construct(AccountInterface $current_user) {
+    $this->currentUser = $current_user;
+  }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    $typed_data = $this->getTypedData();
     if ($typed_data instanceof OptionsProviderInterface) {
-      $account = \Drupal::currentUser();
-      $allowed_values = $typed_data->getSettableValues($account);
+      $allowed_values = $typed_data->getSettableValues($this->currentUser);
       $constraint->choices = $allowed_values;
 
       // If the data is complex, we have to validate its main property.
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php
old mode 100644
new mode 100755
index add46eb..8398ae8
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php
@@ -9,6 +9,8 @@
 
 use Drupal\Core\TypedData\ComplexDataInterface;
 use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait;
+use Drupal\Core\TypedData\Validation\TypedDataPropertyValidationEnvelope;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
 use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@@ -18,35 +20,26 @@
  */
 class ComplexDataConstraintValidator extends ConstraintValidator {
 
+  use TypedDataAwareValidatorTrait;
+
   /**
    * {@inheritdoc}
    */
-  public function validate($value, Constraint $constraint) {
-    if (!isset($value)) {
-      return;
-    }
+  public function validate($data, Constraint $constraint) {
 
     // If un-wrapped data has been passed, fetch the typed data object first.
-    if (!$value instanceof TypedDataInterface) {
-      $value = $this->context->getMetadata()->getTypedData();
+    if (!$data instanceof TypedDataInterface) {
+      $data = $this->getTypedData();
     }
-    if (!$value instanceof ComplexDataInterface) {
-      throw new UnexpectedTypeException($value, 'ComplexData');
+    if (!$data instanceof ComplexDataInterface) {
+      throw new UnexpectedTypeException($data, 'ComplexData');
     }
 
-    $group = $this->context->getGroup();
-
     foreach ($constraint->properties as $name => $constraints) {
-      $property = $value->get($name);
-      $is_container = $property instanceof ComplexDataInterface || $property instanceof ListInterface;
-      if (!$is_container) {
-        $property = $property->getValue();
-      }
-      elseif ($property->isEmpty()) {
-        // @see \Drupal\Core\TypedData\Validation\PropertyContainerMetadata::accept();
-        $property = NULL;
-      }
-      $this->context->validateValue($property, $constraints, $name, $group);
+      $this->context->getValidator()
+        ->inContext($this->context)
+        ->validate($data->get($name), $constraints, NULL, FALSE);
     }
   }
+
 }
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraint.php
new file mode 100644
index 0000000..cdf1b63
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraint.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Validation\Plugin\Validation\Constraint\NotNullConstraint.
+ */
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraints\NotNull;
+
+/**
+ * NotNull constraint.
+ *
+ * Overrides the symfony constraint to handle empty Typed Data structures.
+ *
+ * @Plugin(
+ *   id = "NotNull",
+ *   label = @Translation("NotNull", context = "Validation"),
+ *   type = false
+ * )
+ */
+class NotNullConstraint extends NotNull {
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraintValidator.php
new file mode 100644
index 0000000..8332ed1
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NotNullConstraintValidator.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Validation\Plugin\Validation\Constraint\NotNullConstraintValidator.
+ */
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\ListInterface;
+use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints\NotNullValidator;
+
+/**
+ * NotNull constraint validator.
+ */
+class NotNullConstraintValidator extends NotNullValidator {
+
+  use TypedDataAwareValidatorTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    $typed_data = $this->getTypedData();
+    if (($typed_data instanceof ListInterface || $typed_data instanceof ComplexDataInterface) && $typed_data->isEmpty()) {
+      $value = NULL;
+    }
+    parent::validate($value, $constraint);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NullConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NullConstraint.php
new file mode 100644
index 0000000..3d87128
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NullConstraint.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Validation\Plugin\Validation\Constraint\NullConstraint.
+ */
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Symfony\Component\Validator\Constraints\Null;
+
+/**
+ * Null constraint.
+ *
+ * Overrides the symfony constraint to handle empty Typed Data structures.
+ *
+ * @Plugin(
+ *   id = "Null",
+ *   label = @Translation("Null", context = "Validation"),
+ *   type = false
+ * )
+ */
+class NullConstraint extends Null {
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NullConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NullConstraintValidator.php
new file mode 100644
index 0000000..a6af4e9
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/NullConstraintValidator.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Validation\Plugin\Validation\Constraint\NullConstraintValidator.
+ */
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\ListInterface;
+use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\Constraints\NullValidator;
+
+/**
+ * Null constraint validator.
+ */
+class NullConstraintValidator extends NullValidator {
+
+  use TypedDataAwareValidatorTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    $typed_data = $this->getTypedData();
+    if (($typed_data instanceof ListInterface || $typed_data instanceof ComplexDataInterface) && $typed_data->isEmpty()) {
+      $value = NULL;
+    }
+    parent::validate($value, $constraint);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php
index d1bdc0f..d873820 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php
@@ -15,6 +15,7 @@
 use Drupal\Core\TypedData\Type\IntegerInterface;
 use Drupal\Core\TypedData\Type\StringInterface;
 use Drupal\Core\TypedData\Type\UriInterface;
+use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
 
@@ -23,6 +24,8 @@
  */
 class PrimitiveTypeConstraintValidator extends ConstraintValidator {
 
+  use TypedDataAwareValidatorTrait;
+
   /**
    * Implements \Symfony\Component\Validator\ConstraintValidatorInterface::validate().
    */
@@ -32,7 +35,7 @@ public function validate($value, Constraint $constraint) {
       return;
     }
 
-    $typed_data = $this->context->getMetadata()->getTypedData();
+    $typed_data = $this->getTypedData();
     $valid = TRUE;
     if ($typed_data instanceof BinaryInterface && !is_resource($value)) {
       $valid = FALSE;
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
index a5dde71..d6c3910 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php
@@ -19,7 +19,7 @@ class UniqueFieldValueValidator extends ConstraintValidator {
    * {@inheritdoc}
    */
   public function validate($items, Constraint $constraint) {
-    if (!isset($items)) {
+    if (!$item = $items->first()) {
       return;
     }
     $field_name = $items->getFieldDefinition()->getName();
@@ -31,13 +31,13 @@ public function validate($items, Constraint $constraint) {
     $value_taken = (bool) \Drupal::entityQuery($entity_type_id)
       // The id could be NULL, so we cast it to 0 in that case.
       ->condition($id_key, (int) $items->getEntity()->id(), '<>')
-      ->condition($field_name, $items->first()->value)
+      ->condition($field_name, $item->value)
       ->range(0, 1)
       ->count()
       ->execute();
 
     if ($value_taken) {
-      $this->context->addViolation($constraint->message, array("%value" => $items->value));
+      $this->context->addViolation($constraint->message, array("%value" => $item->value));
     }
   }
 }
diff --git a/core/modules/forum/src/Plugin/Validation/Constraint/ForumLeafConstraintValidator.php b/core/modules/forum/src/Plugin/Validation/Constraint/ForumLeafConstraintValidator.php
index e4e9be2..f2d2400 100644
--- a/core/modules/forum/src/Plugin/Validation/Constraint/ForumLeafConstraintValidator.php
+++ b/core/modules/forum/src/Plugin/Validation/Constraint/ForumLeafConstraintValidator.php
@@ -20,10 +20,10 @@ class ForumLeafConstraintValidator extends ConstraintValidator {
    * {@inheritdoc}
    */
   public function validate($items, Constraint $constraint) {
-    if (!isset($items)) {
-      return;
-    }
     $item = $items->first();
+    if (!isset($item)) {
+      return NULL;
+    }
 
     // Verify that a term has been selected.
     if (!$item->entity) {
diff --git a/core/modules/quickedit/src/Form/QuickEditFieldForm.php b/core/modules/quickedit/src/Form/QuickEditFieldForm.php
index 2db50c4..de8dc5b 100644
--- a/core/modules/quickedit/src/Form/QuickEditFieldForm.php
+++ b/core/modules/quickedit/src/Form/QuickEditFieldForm.php
@@ -17,7 +17,7 @@
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
 use Drupal\user\PrivateTempStoreFactory;
 use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\Validator\ValidatorInterface;
+use Symfony\Component\Validator\Validator\ValidatorInterface;
 
 /**
  * Builds and process a form for editing a single entity field.
@@ -48,7 +48,7 @@ class QuickEditFieldForm extends FormBase {
   /**
    * The typed data validator.
    *
-   * @var \Symfony\Component\Validator\ValidatorInterface
+   * @var \Symfony\Component\Validator\Validator\ValidatorInterface
    */
   protected $validator;
 
@@ -61,7 +61,7 @@ class QuickEditFieldForm extends FormBase {
    *   The module handler.
    * @param \Drupal\Core\Entity\EntityStorageInterface $node_type_storage
    *   The node type storage.
-   * @param \Symfony\Component\Validator\ValidatorInterface $validator
+   * @param \Symfony\Component\Validator\Validator\ValidatorInterface $validator
    *   The typed data validator service.
    */
   public function __construct(PrivateTempStoreFactory $temp_store_factory, ModuleHandlerInterface $module_handler, EntityStorageInterface $node_type_storage, ValidatorInterface $validator) {
@@ -165,7 +165,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) {
     // @todo: Improve this in https://www.drupal.org/node/2395831.
     $typed_entity = $entity->getTypedData();
     $violations = $this->validator
-      ->validateValue($entity, $typed_entity->getConstraints());
+      ->validate($typed_entity, $typed_entity->getConstraints());
 
     foreach ($violations as $violation) {
       $form_state->setErrorByName($violation->getPropertyPath(), $violation->getMessage());
diff --git a/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequired.php b/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequired.php
index 76bc17d..6e9fa3b 100644
--- a/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequired.php
+++ b/core/modules/user/src/Plugin/Validation/Constraint/UserMailRequired.php
@@ -8,6 +8,7 @@
 namespace Drupal\user\Plugin\Validation\Constraint;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidatorInterface;
 use Symfony\Component\Validator\ExecutionContextInterface;
@@ -58,7 +59,7 @@ public function validatedBy() {
   public function validate($items, Constraint $constraint) {
     /** @var \Drupal\Core\Field\FieldItemListInterface $items */
     /** @var \Drupal\user\UserInterface $account */
-    $account = $this->context->getMetadata()->getTypedData()->getEntity();
+    $account = $items->getEntity();
     $existing_value = NULL;
     if ($account->id()) {
       $account_unchanged = \Drupal::entityManager()
diff --git a/core/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php b/core/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php
new file mode 100644
index 0000000..c027e2c
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php
@@ -0,0 +1,239 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\TypedData\RecursiveContextualValidatorTest.
+ */
+
+namespace Drupal\Tests\Core\TypedData;
+
+use Drupal\Core\Cache\NullBackend;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\MapDataDefinition;
+use Drupal\Core\TypedData\TraversableTypedDataInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\Core\TypedData\TypedDataManager;
+use Drupal\Core\TypedData\Validation\ExecutionContextFactory;
+use Drupal\Core\TypedData\Validation\RecursiveValidator;
+use Drupal\Core\Validation\ConstraintManager;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\Validator\Constraints\Callback;
+use Symfony\Component\Validator\ConstraintValidatorFactory;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
+use Symfony\Component\Validator\DefaultTranslator;
+
+/**
+ * Tests \Drupal\Core\TypedData\Validation\RecursiveContextualValidator
+ *
+ * @group typedData
+ * @coversDefaultClass \Drupal\Core\TypedData\Validation\RecursiveContextualValidator
+ */
+class RecursiveContextualValidatorTest extends UnitTestCase {
+
+  /**
+   * @var \Drupal\Core\TypedData\TypedDataManager
+   */
+  protected $typedDataManager;
+
+  /**
+   * @var \Drupal\Core\TypedData\Validation\RecursiveValidator
+   */
+  protected $recursiveValidator;
+
+  /**
+   * @var \Symfony\Component\Validator\ConstraintValidatorFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $validatorFactory;
+
+  /**
+   * @var \Drupal\Core\TypedData\Validation\ExecutionContextFactory
+   */
+  protected $contextFactory;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $cache_backend = new NullBackend('cache');
+    $namespaces = new \ArrayObject([
+      'Drupal\\Core\\TypedData' => $this->root . '/core/lib/Drupal/Core/TypedData',
+      'Drupal\\Core\\Validation' => $this->root . '/core/lib/Drupal/Core/Validation',
+    ]);
+    $module_handler = $this->getMockBuilder('Drupal\Core\Extension\ModuleHandlerInterface')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $class_resolver = $this->getMockBuilder('Drupal\Core\DependencyInjection\ClassResolverInterface')
+      ->disableOriginalConstructor()
+      ->getMock();
+
+    $this->typedDataManager = new TypedDataManager($namespaces, $cache_backend, $module_handler, $class_resolver);
+    $this->typedDataManager->setValidationConstraintManager(
+      new ConstraintManager($namespaces, $cache_backend, $module_handler)
+    );
+    // Typed data definitions access the manager in the container.
+    $container = new ContainerBuilder();
+    $container->set('typed_data_manager', $this->typedDataManager);
+    \Drupal::setContainer($container);
+
+    $translator = new DefaultTranslator();
+    $this->contextFactory = new ExecutionContextFactory($translator);
+    $this->validatorFactory = new ConstraintValidatorFactory();
+    $this->recursiveValidator = new RecursiveValidator($this->contextFactory, $this->validatorFactory, $this->typedDataManager);
+  }
+
+  /**
+   * @covers ::validate
+   *
+   * @expectedException \Exception
+   */
+  public function testValidateWithGroups() {
+    $this->recursiveValidator->validate('test', NULL, 'test group');
+  }
+
+  /**
+   * @covers ::validate
+   *
+   * @expectedException \Exception
+   */
+  public function testValidateWithoutTypedData() {
+    $this->recursiveValidator->validate('test');
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testBasicValidateWithoutConstraints() {
+    $typed_data = $this->typedDataManager->create(DataDefinition::create('string'));
+    $violations = $this->recursiveValidator->validate($typed_data);
+    $this->assertEquals(0, $violations->count());
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testBasicValidateWithConstraint() {
+    $typed_data = $this->typedDataManager->create(
+      DataDefinition::create('string')
+        ->addConstraint('Callback', [
+          'callback' => function ($value, ExecutionContextInterface $context) {
+            $context->addViolation('test violation: ' . $value);
+          }
+        ])
+    );
+    $typed_data->setValue('foo');
+
+    $violations = $this->recursiveValidator->validate($typed_data);
+    $this->assertEquals(1, $violations->count());
+    // Ensure that the right value is passed into the validator.
+    $this->assertEquals('test violation: foo', $violations->get(0)->getMessage());
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testBasicValidateWithMultipleConstraints() {
+    $options = [
+      'callback' => function ($value, ExecutionContextInterface $context) {
+        $context->addViolation('test violation');
+      }
+    ];
+    $typed_data = $this->typedDataManager->create(
+      DataDefinition::create('string')
+        ->addConstraint('Callback', $options)
+        ->addConstraint('NotNull')
+    );
+    $violations = $this->recursiveValidator->validate($typed_data);
+    $this->assertEquals(2, $violations->count());
+  }
+
+  /**
+   * @covers ::validate
+   * @todo: Fix.
+   */
+  public function testPropertiesValidateWithMultipleLevels() {
+
+    $tree = ['value' => ['key1' => 'value1', 'key2' => 'value2', 'key_with_properties' => ['subkey1' => 'subvalue1', 'subkey2' => 'subvalue2']]];
+    $tree['properties'] = [
+      'key1' => [
+        'value' => 'value1',
+      ],
+      'key2' => [
+        'value' => 'value2',
+      ],
+      'key_with_properties' => [
+        'value' => ['subkey1' => 'subvalue1', 'subkey2' => 'subvalue2'],
+        'properties' => [
+          'subkey1' => [
+            'value' => 'subvalue1',
+          ],
+          'subkey2' => [
+            'value' => 'subvalue2',
+          ],
+        ]
+      ],
+    ];
+
+    $typed_data = $this->setupTypedData($tree, 'test_name');
+    $violations = $this->recursiveValidator->validate($typed_data);
+    $this->assertEquals(6, $violations->count());
+
+    $this->assertEquals('violation: 3', $violations->get(0)->getMessage());
+    $this->assertEquals('violation: value1', $violations->get(1)->getMessage());
+    $this->assertEquals('violation: value2', $violations->get(2)->getMessage());
+    $this->assertEquals('violation: 2', $violations->get(3)->getMessage());
+    $this->assertEquals('violation: subvalue1', $violations->get(4)->getMessage());
+    $this->assertEquals('violation: subvalue2', $violations->get(5)->getMessage());
+
+    $this->assertEquals('', $violations->get(0)->getPropertyPath());
+    $this->assertEquals('key1', $violations->get(1)->getPropertyPath());
+    $this->assertEquals('key2', $violations->get(2)->getPropertyPath());
+    $this->assertEquals('key_with_properties', $violations->get(3)->getPropertyPath());
+    $this->assertEquals('key_with_properties.subkey1', $violations->get(4)->getPropertyPath());
+    $this->assertEquals('key_with_properties.subkey2', $violations->get(5)->getPropertyPath());
+  }
+
+  /**
+   * Setups a typed data object used for test purposes.
+   *
+   * @param array $tree
+   *   An array of value, constraints and properties.
+   *
+   * @return \Drupal\Core\TypedData\TypedDataInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected function setupTypedData(array $tree, $name = '') {
+    $callback = function ($value, ExecutionContextInterface $context) {
+      $context->addViolation('violation: ' . (is_array($value) ? count($value) : $value));
+    };
+
+    $tree += ['constraints' => []];
+
+    if (isset($tree['properties'])) {
+      $map_data_definition = MapDataDefinition::create();
+      $map_data_definition->addConstraint('Callback', ['callback' => $callback]);
+      foreach ($tree['properties'] as $property_name => $property) {
+        $sub_typed_data = $this->setupTypedData($property, $property_name);
+        $map_data_definition->setPropertyDefinition($property_name, $sub_typed_data->getDataDefinition());
+      }
+      $typed_data = $this->typedDataManager->create(
+        $map_data_definition,
+        $tree['value'],
+        $name
+      );
+    }
+    else {
+      /** @var \Drupal\Core\TypedData\TypedDataInterface $typed_data */
+      $typed_data = $this->typedDataManager->create(
+        DataDefinition::create('string')
+          ->addConstraint('Callback', ['callback' => $callback]),
+        $tree['value'],
+        $name
+      );
+    }
+
+    return $typed_data;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php b/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php
index 0e90672..49c08e5 100644
--- a/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php
+++ b/core/tests/Drupal/Tests/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidatorTest.php
@@ -24,17 +24,10 @@ class PrimitiveTypeConstraintValidatorTest extends UnitTestCase {
    * @dataProvider provideTestValidate
    */
   public function testValidate(PrimitiveInterface $typed_data, $value, $valid) {
-    $metadata = $this->getMockBuilder('Drupal\Core\TypedData\Validation\Metadata')
-      ->disableOriginalConstructor()
-      ->getMock();
-    $metadata->expects($this->any())
-      ->method('getTypedData')
-      ->willReturn($typed_data);
-
-    $context = $this->getMock('Symfony\Component\Validator\ExecutionContextInterface');
+    $context = $this->getMock('\Symfony\Component\Validator\Context\ExecutionContextInterface');
     $context->expects($this->any())
-      ->method('getMetadata')
-      ->willReturn($metadata);
+      ->method('getObject')
+      ->willReturn($typed_data);
 
     if ($valid) {
       $context->expects($this->never())
diff --git a/core/vendor/symfony/validator/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/core/vendor/symfony/validator/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php
index 191decd..6f82d6d 100644
--- a/core/vendor/symfony/validator/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php
+++ b/core/vendor/symfony/validator/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php
@@ -92,7 +92,7 @@ public function atPath($path)
     /**
      * {@inheritdoc}
      */
-    public function validate($value, $constraints = null, $groups = null)
+    public function validate($data, $constraints = null, $groups = null)
     {
         $groups = $groups ? $this->normalizeGroups($groups) : $this->defaultGroups;
 
@@ -115,9 +115,9 @@ public function validate($value, $constraints = null, $groups = null)
             $metadata->addConstraints($constraints);
 
             $this->validateGenericNode(
-                $value,
+                $data,
                 null,
-                is_object($value) ? spl_object_hash($value) : null,
+                is_object($data) ? spl_object_hash($data) : null,
                 $metadata,
                 $this->defaultPropertyPath,
                 $groups,
@@ -134,9 +134,9 @@ public function validate($value, $constraints = null, $groups = null)
 
         // If an object is passed without explicit constraints, validate that
         // object against the constraints defined for the object's class
-        if (is_object($value)) {
+        if (is_object($data)) {
             $this->validateObject(
-                $value,
+                $data,
                 $this->defaultPropertyPath,
                 $groups,
                 TraversalStrategy::IMPLICIT,
@@ -151,9 +151,9 @@ public function validate($value, $constraints = null, $groups = null)
 
         // If an array is passed without explicit constraints, validate each
         // object in the array
-        if (is_array($value)) {
+        if (is_array($data)) {
             $this->validateEachObjectIn(
-                $value,
+                $data,
                 $this->defaultPropertyPath,
                 $groups,
                 true,
@@ -169,7 +169,7 @@ public function validate($value, $constraints = null, $groups = null)
         throw new RuntimeException(sprintf(
             'Cannot validate values of type "%s" automatically. Please '.
             'provide a constraint.',
-            gettype($value)
+            gettype($data)
         ));
     }
 
