diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
index cd7ad17..2a6a823 100644
--- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php
+++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
@@ -28,7 +28,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;
 
@@ -335,7 +335,7 @@ public function getValidator() {
         ->setMetadataFactory(new MetadataFactory($this))
         ->setTranslator(new DrupalTranslator())
         ->setConstraintValidatorFactory(new ConstraintValidatorFactory($this->classResolver))
-        ->setApiVersion(Validation::API_VERSION_2_4)
+        ->setApiVersion(Validation::API_VERSION_2_5)
         ->getValidator();
     }
     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..bba76bd
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/ConstraintViolationBuilder.php
@@ -0,0 +1,180 @@
+<?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\Util\PropertyPath;
+use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface;
+
+/**
+ * Implements the ConstraintViolationBuilderInterface.
+ */
+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..c55f495
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContext.php
@@ -0,0 +1,309 @@
+<?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\Util\PropertyPath;
+use Symfony\Component\Validator\Validator\ValidatorInterface;
+
+/**
+ *
+ */
+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 typed data object.
+   *
+   * @var TypedDataInterface
+   */
+  protected $value;
+
+  /**
+   * 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 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->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->value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadata() {
+    return $this->metadata;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroup() {
+    return Constraint::DEFAULT_GROUP;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getClassName() {
+    // @todo.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyName() {
+    // @todo.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyPath($subPath = '') {
+    return PropertyPath::append($this->propertyPath, $subPath);
+  }
+
+  /**
+   * {@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($cacheKey, $constraintHash) {
+    $this->validatedConstraints[$cacheKey . ':' . $constraintHash] = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isConstraintValidated($cacheKey, $constraintHash) {
+    return isset($this->validatedConstraints[$cacheKey . ':' . $constraintHash]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateValue($value, $constraints, $subPath = '', $groups = NULL) {
+    throw new \LogicException('Legacy validator API is unsupported.');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function markGroupAsValidated($cacheKey, $groupHash) {
+    $this->validatedObjects[$cacheKey][$groupHash] = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isGroupValidated($cacheKey, $groupHash) {
+    return isset($this->validatedObjects[$cacheKey][$groupHash]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function markObjectAsInitialized($cacheKey) {
+    // Not supported, so nothing todo.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isObjectInitialized($cacheKey) {
+    // 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..1c9313f
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/ExecutionContextFactory.php
@@ -0,0 +1,48 @@
+<?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;
+
+/**
+ * Creates an exeuction context.
+ */
+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/RecursiveContextualValidator.php b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php
new file mode 100644
index 0000000..5bc31be
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/RecursiveContextualValidator.php
@@ -0,0 +1,171 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\PropertyContainerPropertyMetadata.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Drupal\Core\TypedData\TraversableTypedDataInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+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\Util\PropertyPath;
+use Symfony\Component\Validator\Validator\ContextualValidatorInterface;
+
+/**
+ *
+ */
+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 ExecutionContextInterface $context The execution context
+   * @param
+   * @param \Symfony\Component\Validator\ConstraintValidatorFactoryInterface $validator_factory
+   */
+  public function __construct(ExecutionContextInterface $context, MetadataFactoryInterface $factory, ConstraintValidatorFactoryInterface $validator_factory) {
+    $this->context = $context;
+    $this->defaultPropertyPath = $context->getPropertyPath();
+    $this->metadataFactory = $factory;
+    $this->constraintValidatorFactory = $validator_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function atPath($path) {
+    $this->defaultPropertyPath = $this->context->getPropertyPath($path);
+
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, $constraints = NULL, $groups = NULL) {
+    if (isset($groups)) {
+      throw new \LogicException('Passing custom groups is not supported.');
+    }
+
+    if (!$value 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($value, $constraints);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validateNode(TypedDataInterface $data, $constraints = NULL) {
+    $previousValue = $this->context->getValue();
+    $previousObject = $this->context->getObject();
+    $previousMetadata = $this->context->getMetadata();
+    $previousPath = $this->context->getPropertyPath();
+
+    $metadata = $this->metadataFactory->getMetadataFor($data);
+    $cacheKey = spl_object_hash($data);
+    $propertyPath = PropertyPath::append($previousPath, $data->getName());
+    $this->context->setNode($data, $data, $metadata, $propertyPath);
+
+    if (!$this->context->isGroupValidated($cacheKey, Constraint::DEFAULT_GROUP)) {
+      $this->context->markGroupAsValidated($cacheKey, Constraint::DEFAULT_GROUP);
+
+      $constraints = isset($constraints) ? $constraints : $metadata->findConstraints(Constraint::DEFAULT_GROUP);
+      $this->validateConstraints($data, $cacheKey, $constraints);
+    }
+
+    // Validate properties, if any.
+    if ($data instanceof TraversableTypedDataInterface) {
+      foreach ($data as $name => $property) {
+        $this->validateNode($property);
+      }
+    }
+
+    $this->context->setNode($previousValue, $previousObject, $previousMetadata, $previousPath);
+
+    return $this;
+  }
+
+  /**
+   * Validates a node's value against all constraints in the given group.
+   *
+   * @param mixed                     $value    The validated value
+   */
+  protected function validateConstraints($value, $cacheKey, $constraints) {
+    foreach ($constraints as $constraint) {
+      // Prevent duplicate validation of constraints, in the case
+      // that constraints belong to multiple validated groups
+      if (null !== $cacheKey) {
+        $constraintHash = spl_object_hash($constraint);
+
+        if ($this->context->isConstraintValidated($cacheKey, $constraintHash)) {
+          continue;
+        }
+
+        $this->context->markConstraintAsValidated($cacheKey, $constraintHash);
+      }
+
+      $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 (!is_object($object)) {
+      throw new \InvalidArgumentException('Passing class name is not supported.');
+    }
+    elseif (!$object instanceof TraversableTypedDataInterface) {
+      throw new \InvalidArgumentException('Passed data does not contain properties.');
+    }
+    return $this->validateNode($object->get($propertyName));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validatePropertyValue($object, $propertyName, $value, $groups = NULL) {
+    // @todo: Fetch constraints from the property and validate.
+  }
+
+}
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..e0e8ded
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/RecursiveValidator.php
@@ -0,0 +1,109 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\RecursiveValidator.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Drupal\Core\TypedData\TypedDataInterface;
+use Symfony\Component\Validator\ConstraintValidatorFactoryInterface;
+use Symfony\Component\Validator\Context\ExecutionContextFactoryInterface;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
+use Symfony\Component\Validator\Validator\ValidatorInterface;
+
+/**
+ *
+ */
+class RecursiveValidator implements ValidatorInterface {
+
+  /**
+   * @var ExecutionContextFactoryInterface
+   */
+  protected $contextFactory;
+
+  /**
+   * @var \Symfony\Component\Validator\ConstraintValidatorFactoryInterface
+   */
+  protected $constraintValidatorFactory;
+
+  /**
+   * Creates a new validator.
+   *
+   * @param ExecutionContextFactoryInterface $contextFactory The factory for
+   *                                                                creating new contexts
+   * @param \Symfony\Component\Validator\ConstraintValidatorFactoryInterface $validator_factory
+   */
+  public function __construct(ExecutionContextFactoryInterface $context_factory, ConstraintValidatorFactoryInterface $validator_factory) {
+    $this->contextFactory = $context_factory;
+    $this->constraintValidatorFactory = $validator_factory;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function startContext($root = NULL) {
+    return new RecursiveContextualValidator($this->contextFactory->createContext($this, $root), $this, $this->constraintValidatorFactory);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function inContext(ExecutionContextInterface $context) {
+    return new RecursiveContextualValidator($context, $this, $this->constraintValidatorFactory);
+  }
+
+  /**
+   * {@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..4b61207
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/TypedDataAwareValidatorTrait.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\TypedDataAwareValidatorTrait.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+/**
+ * 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 */
+    $metadata = $context->getMetadata();
+    if ($metadata instanceof TypedDataMetadataInterface) {
+      return $metadata->getTypedData();
+    }
+    else {
+      throw new \LogicException("The validator is not using the Typed Data metadata classes.");
+    }
+  }
+
+}
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..14f6b21
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadata.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\MetadataBase.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\Core\TypedData\TypedDataManager;
+use Symfony\Component\Validator\Exception\BadMethodCallException;
+use Symfony\Component\Validator\Mapping\CascadingStrategy;
+use Symfony\Component\Validator\Mapping\TraversalStrategy;
+use Symfony\Component\Validator\ValidationVisitorInterface;
+
+/**
+ * Validator metadata for typed data objects.
+ */
+class TypedDataMetadata implements TypedDataMetadataInterface {
+
+  /**
+   * 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 getTypedData() {
+    return $this->typedData;
+  }
+
+  /**
+   * {@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/TypedData/Validation/TypedDataMetadataInterface.php b/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadataInterface.php
new file mode 100644
index 0000000..03a9a95
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Validation/TypedDataMetadataInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\TypedData\Validation\TypedDataMetadataInterface.
+ */
+
+namespace Drupal\Core\TypedData\Validation;
+
+use Symfony\Component\Validator\Mapping\MetadataInterface;
+
+/**
+ * Interface for Typed Data validation metadata.
+ *
+ * As each typed data object can serve as validation root, the metadata has to
+ * implement ClassMetadataInterface.
+ */
+interface TypedDataMetadataInterface extends MetadataInterface {
+
+  /**
+   * Returns the typed data object.
+   *
+   * @return \Drupal\Core\TypedData\TypedDataInterface
+   *   The typed data object.
+   */
+  public function getTypedData();
+
+}
diff --git a/core/lib/Drupal/Core/Validation/ConstraintManager.php b/core/lib/Drupal/Core/Validation/ConstraintManager.php
index c760d1e..07094cd 100644
--- a/core/lib/Drupal/Core/Validation/ConstraintManager.php
+++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php
@@ -79,16 +79,6 @@ 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',
-      'type' => FALSE,
-    ));
     $this->discovery->setDefinition('Blank', array(
       'label' => new TranslationWrapper('Blank'),
       'class' => '\Symfony\Component\Validator\Constraints\Blank',
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..ef1fce8 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,46 @@
 
 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;
+
+  /**
+   * 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->context->getMetadata()->getTypedData();
-
+    $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.
@@ -49,4 +70,11 @@ public function validate($value, Constraint $constraint) {
     parent::validate($value, $constraint);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static($container->get('current_user'));
+  }
+
 }
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..16bdf59
--- 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,6 +20,8 @@
  */
 class ComplexDataConstraintValidator extends ConstraintValidator {
 
+  use TypedDataAwareValidatorTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -28,25 +32,17 @@ public function validate($value, Constraint $constraint) {
 
     // If un-wrapped data has been passed, fetch the typed data object first.
     if (!$value instanceof TypedDataInterface) {
-      $value = $this->context->getMetadata()->getTypedData();
+      $value = $this->getTypedData();
     }
     if (!$value instanceof ComplexDataInterface) {
       throw new UnexpectedTypeException($value, '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($value->get($name), $constraints);
     }
   }
+
 }
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..68d96ed 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($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/modules/user/src/Tests/UserValidationTest.php b/core/modules/user/src/Tests/UserValidationTest.php
index ec44a79..172b79d 100644
--- a/core/modules/user/src/Tests/UserValidationTest.php
+++ b/core/modules/user/src/Tests/UserValidationTest.php
@@ -122,9 +122,9 @@ function testValidation() {
     //   https://drupal.org/node/2023465.
     $this->assertEqual(count($violations), 2, 'Violations found when email is too long');
     $this->assertEqual($violations[0]->getPropertyPath(), 'mail.0.value');
-    $this->assertEqual($violations[0]->getMessage(), t('%name: the email address can not be longer than @max characters.', array('%name' => $user->get('mail')->getFieldDefinition()->getLabel(), '@max' => Email::EMAIL_MAX_LENGTH)));
+    $this->assertEqual($violations[0]->getMessage(), t('This value is not a valid email address.'));
     $this->assertEqual($violations[1]->getPropertyPath(), 'mail.0.value');
-    $this->assertEqual($violations[1]->getMessage(), t('This value is not a valid email address.'));
+    $this->assertEqual($violations[1]->getMessage(), t('%name: the email address can not be longer than @max characters.', array('%name' => $user->get('mail')->getFieldDefinition()->getLabel(), '@max' => Email::EMAIL_MAX_LENGTH)));
 
     // Provoke an email collision with an existing user.
     $user->set('mail', 'existing@example.com');
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..fcd14b7
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/TypedData/RecursiveContextualValidatorTest.php
@@ -0,0 +1,221 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Core\TypedData\RecursiveContextualValidatorTest.
+ */
+
+namespace Drupal\Tests\Core\TypedData;
+
+use Drupal\Core\TypedData\TraversableTypedDataInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\Core\TypedData\Validation\ExecutionContextFactory;
+use Drupal\Core\TypedData\Validation\RecursiveValidator;
+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
+ */
+class RecursiveContextualValidatorTest extends UnitTestCase {
+
+  /**
+   * @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();
+
+    $translator = new DefaultTranslator();
+    $this->contextFactory = new ExecutionContextFactory($translator);
+    $this->validatorFactory = new ConstraintValidatorFactory();
+    $this->recursiveValidator = new RecursiveValidator($this->contextFactory, $this->validatorFactory);
+  }
+
+  /**
+   * @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->getMock('\Drupal\Core\TypedData\TypedDataInterface');
+    $typed_data->expects($this->any())
+      ->method('getConstraints')
+      ->willReturn([]);
+    $violations = $this->recursiveValidator->validate($typed_data);
+    $this->assertEquals(0, $violations->count());
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testBasicValidateWithConstraint() {
+    $callback_constraint = new Callback([
+      'callback' => function ($value, ExecutionContextInterface $context) {
+        $context->addViolation('test violation: ' . spl_object_hash($value));
+      }
+    ]);
+
+    $typed_data = $this->getMock('\Drupal\Core\TypedData\TypedDataInterface');
+    $typed_data->expects($this->any())
+      ->method('getConstraints')
+      ->willReturn([$callback_constraint]);
+
+    $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: ' . spl_object_hash($typed_data), $violations->get(0)->getMessage());
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testBasicValidateWithMultipleConstraints() {
+    $callback_constraint = new Callback([
+      'callback' => function ($value, ExecutionContextInterface $context) {
+        $context->addViolation('test violation');
+      }
+    ]);
+
+    $typed_data = $this->getMock('\Drupal\Core\TypedData\TypedDataInterface');
+    $typed_data->expects($this->any())
+      ->method('getConstraints')
+      ->willReturn([$callback_constraint, clone $callback_constraint]);
+
+    $violations = $this->recursiveValidator->validate($typed_data);
+    $this->assertEquals(2, $violations->count());
+  }
+
+  /**
+   * @covers ::validate
+   */
+  public function testPropertiesValidateWithMultipleLevels() {
+    $callback_constraint = new Callback([
+      'callback' => function (TypedDataInterface $value, ExecutionContextInterface $context) {
+        $context->addViolation('violation: ' . $value->getValue());
+      }
+    ]);
+
+    $tree = ['value' => []];
+    $tree['properties'] = [
+      'key1' => [
+        'value' => 'value1',
+        'constraints' => [clone $callback_constraint],
+      ],
+      'key2' => [
+        'value' => 'value2',
+        'constraints' => [clone $callback_constraint]
+      ],
+      'key_with_properties' => [
+        'value' => 'value_with_properties',
+        'constraints' => [clone $callback_constraint],
+        'properties' => [
+          'subkey1' => [
+            'value' => 'subvalue1',
+            'constraints' => [clone $callback_constraint]
+          ],
+          'subkey2' => [
+            'value' => 'subvalue2',
+            'constraints' => [clone $callback_constraint]
+          ],
+        ]
+      ],
+    ];
+
+    $typed_data = $this->setupTypedData($tree);
+    $violations = $this->recursiveValidator->validate($typed_data);
+    $this->assertEquals(5, $violations->count());
+
+    $this->assertEquals('violation: value1', $violations->get(0)->getMessage());
+    $this->assertEquals('violation: value2', $violations->get(1)->getMessage());
+    $this->assertEquals('violation: value_with_properties', $violations->get(2)->getMessage());
+    $this->assertEquals('violation: subvalue1', $violations->get(3)->getMessage());
+    $this->assertEquals('violation: subvalue2', $violations->get(4)->getMessage());
+
+    $this->assertEquals('key1', $violations->get(0)->getPropertyPath());
+    $this->assertEquals('key2', $violations->get(1)->getPropertyPath());
+    $this->assertEquals('key_with_properties', $violations->get(2)->getPropertyPath());
+    $this->assertEquals('key_with_properties.subkey1', $violations->get(3)->getPropertyPath());
+    $this->assertEquals('key_with_properties.subkey2', $violations->get(4)->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 = '') {
+    $tree += ['constraints' => []];
+
+    if (isset($tree['properties'])) {
+      $properties = [];
+      foreach ($tree['properties'] as $property_name => $property) {
+        $sub_typed_data = $this->setupTypedData($property, $property_name);
+        $properties[$property_name] = $sub_typed_data;
+      }
+      $typed_data = $this->getMock('\Drupal\Tests\Core\TypedData\TraversableTypedDataInterfaceWithIterator');
+      $typed_data->expects($this->any())
+        ->method('getIterator')
+        ->willReturn(new \ArrayIterator($properties));
+    }
+    else {
+      $typed_data = $this->getMock('\Drupal\Core\TypedData\TypedDataInterface');
+    }
+
+    $typed_data->expects($this->any())
+      ->method('getValue')
+      ->willReturn($tree['value']);
+    $typed_data->expects($this->any())
+      ->method('getConstraints')
+      ->willReturn($tree['constraints']);
+    $typed_data->expects($this->any())
+      ->method('getName')
+      ->willReturn($name);
+
+    return $typed_data;
+  }
+
+}
+
+interface TraversableTypedDataInterfaceWithIterator extends TraversableTypedDataInterface, \IteratorAggregate {
+
+}
+
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..3a73b60 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,7 +24,7 @@ class PrimitiveTypeConstraintValidatorTest extends UnitTestCase {
    * @dataProvider provideTestValidate
    */
   public function testValidate(PrimitiveInterface $typed_data, $value, $valid) {
-    $metadata = $this->getMockBuilder('Drupal\Core\TypedData\Validation\Metadata')
+    $metadata = $this->getMockBuilder('Drupal\Core\TypedData\Validation\GenericMetadata')
       ->disableOriginalConstructor()
       ->getMock();
     $metadata->expects($this->any())
