diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraintValidator.php
index c6743c8..4501623 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraintValidator.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/BundleConstraintValidator.php
@@ -19,22 +19,11 @@ class BundleConstraintValidator extends ConstraintValidator {
   /**
    * {@inheritdoc}
    */
-  public function validate($entity_adapter, Constraint $constraint) {
-    if (!isset($entity_adapter)) {
+  public function validate($entity, Constraint $constraint) {
+    if (!isset($entity)) {
       return;
     }
 
-    // @todo The $entity_adapter parameter passed to this function should always
-    //   be a typed data object, but due to a bug, the unwrapped entity is
-    //   passed for the computed entity property of entity reference fields.
-    //   Remove this after fixing that in https://www.drupal.org/node/2346373.
-    if (!$entity_adapter instanceof TypedDataInterface) {
-      $entity = $entity_adapter;
-    }
-    else {
-      $entity = $entity_adapter->getValue();
-    }
-
     if (!in_array($entity->bundle(), $constraint->getBundleOption())) {
       $this->context->addViolation($constraint->message, array('%bundle' => implode(', ', $constraint->getBundleOption())));
     }
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityTypeConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityTypeConstraintValidator.php
index 5138856..36476df 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityTypeConstraintValidator.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityTypeConstraintValidator.php
@@ -17,24 +17,13 @@
 class EntityTypeConstraintValidator extends ConstraintValidator {
 
   /**
-   * Implements \Symfony\Component\Validator\ConstraintValidatorInterface::validate().
+   * {@inheritdoc}
    */
-  public function validate($entity_adapter, Constraint $constraint) {
-    if (!isset($entity_adapter)) {
+  public function validate($entity, Constraint $constraint) {
+    if (!isset($entity)) {
       return;
     }
 
-    // @todo The $entity_adapter parameter passed to this function should always
-    //   be a typed data object, but due to a bug, the unwrapped entity is
-    //   passed for the computed entity property of entity reference fields.
-    //   Remove this after fixing that in https://www.drupal.org/node/2346373.
-    if (!$entity_adapter instanceof TypedDataInterface) {
-      $entity = $entity_adapter;
-    }
-    else {
-      $entity = $entity_adapter->getValue();
-    }
-
     /** @var $entity \Drupal\Core\Entity\EntityInterface */
     if ($entity->getEntityTypeId() != $constraint->type) {
       $this->context->addViolation($constraint->message, array('%type' => $constraint->type));
diff --git a/core/lib/Drupal/Core/Field/FieldItemBase.php b/core/lib/Drupal/Core/Field/FieldItemBase.php
index 1bb5486..c102d60 100644
--- a/core/lib/Drupal/Core/Field/FieldItemBase.php
+++ b/core/lib/Drupal/Core/Field/FieldItemBase.php
@@ -280,4 +280,13 @@ public static function onDependencyRemoval(FieldDefinitionInterface $field_defin
     return FALSE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function unwrap() {
+    // Field items directly implement the TypedDataInterface, so keep the object
+    // when Typed Data is unwrapped.
+    return $this;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Field/FieldItemList.php b/core/lib/Drupal/Core/Field/FieldItemList.php
index 930b4e1..d888ad3 100644
--- a/core/lib/Drupal/Core/Field/FieldItemList.php
+++ b/core/lib/Drupal/Core/Field/FieldItemList.php
@@ -370,4 +370,13 @@ protected function defaultValueWidget(FormStateInterface $form_state) {
     return $form_state->get('default_value_widget');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function unwrap() {
+    // Field item lists directly implement the TypedDataInterface, so keep the
+    // object when Typed Data is unwrapped.
+    return $this;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Plugin/Context/Context.php b/core/lib/Drupal/Core/Plugin/Context/Context.php
index a0ad01f..4bcee6f 100644
--- a/core/lib/Drupal/Core/Plugin/Context/Context.php
+++ b/core/lib/Drupal/Core/Plugin/Context/Context.php
@@ -46,7 +46,7 @@ public function getContextValue() {
       }
       return NULL;
     }
-    return $this->contextData->getValue();
+    return $this->contextData->unwrap();
   }
 
   /**
diff --git a/core/lib/Drupal/Core/TypedData/TypedData.php b/core/lib/Drupal/Core/TypedData/TypedData.php
index 3216d21..7633d48 100644
--- a/core/lib/Drupal/Core/TypedData/TypedData.php
+++ b/core/lib/Drupal/Core/TypedData/TypedData.php
@@ -200,4 +200,12 @@ public function getPropertyPath() {
   public function getParent() {
     return $this->parent;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function unwrap() {
+    return $this->getValue();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/TypedData/TypedDataInterface.php b/core/lib/Drupal/Core/TypedData/TypedDataInterface.php
index 7b038e5..95d9c7f 100644
--- a/core/lib/Drupal/Core/TypedData/TypedDataInterface.php
+++ b/core/lib/Drupal/Core/TypedData/TypedDataInterface.php
@@ -164,4 +164,24 @@ public function getPropertyPath();
    *   root of a typed data tree. Defaults to NULL.
    */
   public function setContext($name = NULL, TraversableTypedDataInterface $parent = NULL);
+
+  /**
+   * Unwraps a data object if needed.
+   *
+   * Unwrapping removes any Typed Data objects wrapping the data in its regular
+   * representation; e.g., a string or an array. However, if data is regularly
+   * represented as an object and this objects directly implement the Typed
+   * Data API, there is no wrapping object to remove and the object itself is
+   * returned.
+   *
+   * This may be used to ensure data is in the right representation before it is
+   * passed on to other code components. E.g., when typed data is validated it
+   * gets unwrapped before it is passed on to constraint validators, such that
+   * entity objects get passed unwrapped and fields stay objects (which directly
+   * implemented TypedDataInterface).
+   *
+   * @return mixed
+   *   The data, without any wrapping Typed Data object.
+   */
+  public function unwrap();
 }
diff --git a/core/lib/Drupal/Core/TypedData/Validation/Metadata.php b/core/lib/Drupal/Core/TypedData/Validation/Metadata.php
index 73bfc8b..e5613f4 100644
--- a/core/lib/Drupal/Core/TypedData/Validation/Metadata.php
+++ b/core/lib/Drupal/Core/TypedData/Validation/Metadata.php
@@ -62,7 +62,7 @@ public function accept(ValidationVisitorInterface $visitor, $typed_data, $group,
     // @todo: Do we have to care about groups? Symfony class metadata has
     // $propagatedGroup.
 
-    $visitor->visit($this, $typed_data->getValue(), $group, $propertyPath);
+    $visitor->visit($this, $typed_data->unwrap(), $group, $propertyPath);
   }
 
   /**
@@ -89,7 +89,7 @@ public function getPropertyName() {
    * @return mixed The value of the property.
    */
   public function getPropertyValue($container) {
-    return $this->typedData->getValue();
+    return $this->typedData->unwrap();
   }
 
   /**
diff --git a/core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php b/core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php
index f5850eb..cbbbb2d 100644
--- a/core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php
+++ b/core/lib/Drupal/Core/TypedData/Validation/PropertyContainerMetadata.php
@@ -25,12 +25,16 @@ public function accept(ValidationVisitorInterface $visitor, $typed_data, $group,
     // if the data structure is empty. That way existing NotNull or NotBlank
     // constraints work as expected.
     if ($typed_data->isEmpty()) {
-      $typed_data = NULL;
+      $data = NULL;
     }
-    $visitor->visit($this, $typed_data, $group, $propertyPath);
+    else {
+      $data = $typed_data->unwrap();
+    }
+    $visitor->visit($this, $data, $group, $propertyPath);
     $pathPrefix = isset($propertyPath) && $propertyPath !== '' ? $propertyPath . '.' : '';
 
-    if ($typed_data) {
+    // 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);
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php
index 31ffc5b..add46eb 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
 
 use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
 use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@@ -25,6 +26,10 @@ public function validate($value, Constraint $constraint) {
       return;
     }
 
+    // If un-wrapped data has been passed, fetch the typed data object first.
+    if (!$value instanceof TypedDataInterface) {
+      $value = $this->context->getMetadata()->getTypedData();
+    }
     if (!$value instanceof ComplexDataInterface) {
       throw new UnexpectedTypeException($value, 'ComplexData');
     }
