diff --git a/core/lib/Drupal/Core/Entity/EntityNG.php b/core/lib/Drupal/Core/Entity/EntityNG.php
index 6a93a59..bbc4507 100644
--- a/core/lib/Drupal/Core/Entity/EntityNG.php
+++ b/core/lib/Drupal/Core/Entity/EntityNG.php
@@ -527,4 +527,12 @@ public function label($langcode = NULL) {
     }
     return $label;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate() {
+    // @todo: Add the typed data manager as proper dependency.
+    return \Drupal::typedData()->getValidator()->validate($this);
+  }
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php
index 377a002..e189248 100644
--- a/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityStorageControllerInterface.php
@@ -153,6 +153,14 @@ public function save(EntityInterface $entity);
    *   - translatable: Whether the field is translatable. Defaults to FALSE.
    *   - configurable: A boolean indicating whether the field is configurable
    *     via field.module. Defaults to FALSE.
+   *   - property_constraints: An array of constraint arrays applying to the
+   *     field item properties, keyed by property name. E.g. the following
+   *     validates the value property to have a maximum length of 128:
+   *     @code
+   *     array(
+   *       'value' => array('Length' => array('max' => 128)),
+   *     )
+   *     @endcode
    *
    * @see Drupal\Core\TypedData\TypedDataManager::create()
    * @see typed_data()
diff --git a/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php b/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php
index 2c3feef..c9f3700 100644
--- a/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php
+++ b/core/lib/Drupal/Core/Entity/Field/FieldItemBase.php
@@ -91,6 +91,7 @@ public function set($property_name, $value, $notify = TRUE) {
     // value that needs to be updated.
     if (isset($this->properties[$property_name])) {
       $this->properties[$property_name]->setValue($value, FALSE);
+      unset($this->values[$property_name]);
     }
     // Allow setting plain values for not-defined properties also.
     else {
@@ -136,4 +137,19 @@ public function onChange($property_name) {
     // updated property object.
     unset($this->values[$property_name]);
   }
-}
\ No newline at end of file
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConstraints() {
+    $constraints = parent::getConstraints();
+    // If property constraints are present add in a ComplexData constraint for
+    // applying them.
+    if (!empty($this->definition['property_constraints'])) {
+      $constraints[] = \Drupal::typedData()->getValidationConstraintManager()
+        ->create('ComplexData', $this->definition['property_constraints']);
+    }
+    return $constraints;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Field/Type/Field.php b/core/lib/Drupal/Core/Entity/Field/Type/Field.php
index 4829bb7..469bab7 100644
--- a/core/lib/Drupal/Core/Entity/Field/Type/Field.php
+++ b/core/lib/Drupal/Core/Entity/Field/Type/Field.php
@@ -194,4 +194,19 @@ public function defaultAccess($operation = 'view', User $account = NULL) {
     // Grant access per default.
     return TRUE;
   }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConstraints() {
+    // Constraints usually apply to the field item, but required does make
+    // sense on the field only. So we special-case it to apply to the field for
+    // now.
+    // @todo: Separate list and list item definitions to separate constraints.
+    $constraints = array();
+    if (!empty($this->definition['required'])) {
+      $constraints[] = \Drupal::typedData()->getValidationConstraintManager()->create('NotNull', array());
+    }
+    return $constraints;
+  }
 }
diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
index 195e32d..4bfc5c9 100644
--- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php
+++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
@@ -304,28 +304,6 @@ public function getValidationConstraintManager() {
   }
 
   /**
-   * Creates a validation constraint plugin.
-   *
-   * @param string $name
-   *   The name or plugin id of the constraint.
-   * @param mixed $options
-   *   The options to pass to the constraint class. Required and supported
-   *   options depend on the constraint class.
-   *
-   * @return \Symfony\Component\Validator\Constraint
-   *   A validation constraint plugin.
-   */
-  public function createValidationConstraint($name, $options) {
-    if (!is_array($options)) {
-      // Plugins need an array as configuration, so make sure we have one.
-      // The constraint classes support passing the options as part of the
-      // 'value' key also.
-      $options = array('value' => $options);
-    }
-    return $this->getValidationConstraintManager()->createInstance($name, $options);
-  }
-
-  /**
    * Gets configured constraints from a data definition.
    *
    * Any constraints defined for the data type, i.e. below the 'constraint' key
@@ -365,29 +343,28 @@ public function createValidationConstraint($name, $options) {
    */
   public function getConstraints($definition) {
     $constraints = array();
-    // @todo: Figure out how to handle nested constraint structures as
-    // collections.
+    $validation_manager = $this->getValidationConstraintManager();
+
     $type_definition = $this->getDefinition($definition['type']);
     // Auto-generate a constraint for the primitive type if we have a mapping.
     if (isset($type_definition['primitive type'])) {
-      $constraints[] = $this->getValidationConstraintManager()->
-        createInstance('PrimitiveType', array('type' => $type_definition['primitive type']));
+      $constraints[] = $validation_manager->create('PrimitiveType', array('type' => $type_definition['primitive type']));
     }
     // Add in constraints specified by the data type.
     if (isset($type_definition['constraints'])) {
       foreach ($type_definition['constraints'] as $name => $options) {
-        $constraints[] = $this->createValidationConstraint($name, $options);
+        $constraints[] = $validation_manager->create($name, $options);
       }
     }
     // Add any constraints specified as part of the data definition.
     if (isset($definition['constraints'])) {
       foreach ($definition['constraints'] as $name => $options) {
-        $constraints[] = $this->createValidationConstraint($name, $options);
+        $constraints[] = $validation_manager->create($name, $options);
       }
     }
     // Add the NotNull constraint for required data.
     if (!empty($definition['required']) && empty($definition['constraints']['NotNull'])) {
-      $constraints[] = $this->createValidationConstraint('NotNull', array());
+      $constraints[] = $validation_manager->create('NotNull', array());
     }
     return $constraints;
   }
diff --git a/core/lib/Drupal/Core/Validation/ConstraintManager.php b/core/lib/Drupal/Core/Validation/ConstraintManager.php
index da84d55..859cfc1 100644
--- a/core/lib/Drupal/Core/Validation/ConstraintManager.php
+++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php
@@ -56,6 +56,28 @@ public function __construct(\Traversable $namespaces) {
   }
 
   /**
+   * Creates a validation constraint.
+   *
+   * @param string $name
+   *   The name or plugin id of the constraint.
+   * @param mixed $options
+   *   The options to pass to the constraint class. Required and supported
+   *   options depend on the constraint class.
+   *
+   * @return \Symfony\Component\Validator\Constraint
+   *   A validation constraint plugin.
+   */
+  public function create($name, $options) {
+    if (!is_array($options)) {
+      // Plugins need an array as configuration, so make sure we have one.
+      // The constraint classes support passing the options as part of the
+      // 'value' key also.
+      $options = array('value' => $options);
+    }
+    return $this->createInstance($name, $options);
+  }
+
+  /**
    * Callback for registering definitions for constraints shipped with Symfony.
    *
    * @see ConstraintManager::__construct()
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraint.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraint.php
new file mode 100644
index 0000000..5eb7f35
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraint.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Validation\Plugin\Validation\Constraint\CollectionConstraint.
+ */
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Component\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+use Symfony\Component\Validator\Constraint;
+
+/**
+ * Complex data constraint.
+ *
+ * Validates properties of complex data structures.
+ *
+ * @Plugin(
+ *   id = "ComplexData",
+ *   label = @Translation("Complex data", context = "Validation")
+ * )
+ */
+class ComplexDataConstraint extends Constraint {
+
+  /**
+   * An array of constraints for contained properties, keyed by property name.
+   *
+   * @var array
+   */
+  public $properties;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($options = NULL) {
+    // Allow skipping the 'properties' key in the options.
+    if (is_array($options) && !array_key_exists('properties', $options)) {
+      $options = array('properties' => $options);
+    }
+    parent::__construct($options);
+    $constraint_manager = \Drupal::service('validation.constraint');
+
+    // Instantiate constraint objects for array definitions.
+    foreach ($this->properties as &$constraints) {
+      foreach ($constraints as $id => $options) {
+        if (!is_object($options)) {
+          $constraints[$id] = $constraint_manager->create($id, $options);
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefaultOption() {
+    return 'properties';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRequiredOptions() {
+    return array('properties');
+  }
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php
new file mode 100644
index 0000000..31ffc5b
--- /dev/null
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/ComplexDataConstraintValidator.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Validation\Plugin\Validation\Constraint\ComplexDataConstraintValidator.
+ */
+
+namespace Drupal\Core\Validation\Plugin\Validation\Constraint;
+
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Symfony\Component\Validator\Constraint;
+use Symfony\Component\Validator\ConstraintValidator;
+use Symfony\Component\Validator\Exception\UnexpectedTypeException;
+
+/**
+ * Validates complex data.
+ */
+class ComplexDataConstraintValidator extends ConstraintValidator {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validate($value, Constraint $constraint) {
+    if (!isset($value)) {
+      return;
+    }
+
+    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);
+    }
+  }
+}
diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php
new file mode 100644
index 0000000..1f820bc
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityValidationTest.php
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Tests\Entity\EntityValidationTest.
+ */
+
+namespace Drupal\system\Tests\Entity;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\Field\FieldInterface;
+use Drupal\Core\Entity\Field\FieldItemInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+
+/**
+ * Tests Entity API base functionality.
+ */
+class EntityValidationTest extends EntityUnitTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('filter', 'text');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Entity Validation API',
+      'description' => 'Tests the Entity Validation API',
+      'group' => 'Entity API',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->installSchema('user', array('users_roles', 'users_data'));
+    $this->installSchema('entity_test', array(
+      'entity_test_mul',
+      'entity_test_mul_property_data',
+      'entity_test_rev',
+      'entity_test_rev_revision',
+      'entity_test_mulrev',
+      'entity_test_mulrev_property_data',
+      'entity_test_mulrev_property_revision'
+    ));
+
+    // Create the test field.
+    entity_test_install();
+
+    // Install required default configuration for filter module.
+    $this->installConfig(array('system', 'filter'));
+  }
+
+  /**
+   * Creates a test entity.
+   *
+   * @param string $entity_type
+   *   An entity type.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface
+   *   The created test entity.
+   */
+  protected function createTestEntity($entity_type) {
+    $this->entity_name = $this->randomName();
+    $this->entity_user = $this->createUser();
+    $this->entity_field_text = $this->randomName();
+
+    // Pass in the value of the name field when creating. With the user
+    // field we test setting a field after creation.
+    $entity = entity_create($entity_type, array());
+    $entity->user_id->target_id = $this->entity_user->uid;
+    $entity->name->value = $this->entity_name;
+
+    // Set a value for the test field.
+    $entity->field_test_text->value = $this->entity_field_text;
+
+    return $entity;
+  }
+
+  /**
+   * Tests validating test entity types.
+   */
+  public function testValidation() {
+    // All entity variations have to have the same results.
+    foreach (entity_test_entity_types() as $entity_type) {
+      $this->checkValidation($entity_type);
+    }
+  }
+
+  /**
+   * Executes the validation test set for a defined entity type.
+   *
+   * @param string $entity_type
+   *   The entity type to run the tests with.
+   */
+  protected function checkValidation($entity_type) {
+    $entity = $this->createTestEntity($entity_type);
+    $violations = $entity->validate();
+    $this->assertEqual($violations->count(), 0, 'Validation passes.');
+
+    // Test triggering a fail for each of the constraints specified.
+    $test_entity = clone $entity;
+    $test_entity->uuid->value = $this->randomString(129);
+    $this->assertEqual($test_entity->validate()->count(), 1, 'Validation failed.');
+
+    $test_entity = clone $entity;
+    $test_entity->langcode->value = $this->randomString(13);
+    $this->assertEqual($test_entity->validate()->count(), 1, 'Validation failed.');
+
+    $test_entity = clone $entity;
+    $test_entity->type->value = NULL;
+    $this->assertEqual($test_entity->validate()->count(), 1, 'Validation failed.');
+
+    $test_entity = clone $entity;
+    $test_entity->name->value = $this->randomString(33);
+    $this->assertEqual($test_entity->validate()->count(), 1, 'Validation failed.');
+
+    // Make sure the violation is correct.
+    $violations = $test_entity->validate();
+    $violation = $violations->get(0);
+    $this->assertEqual($violation->getRoot(), $test_entity, 'Violation root is entity.');
+    $this->assertEqual($violation->getPropertyPath(), 'name.0.value', 'Violation property path is correct.');
+
+    $this->assertEqual($violation->getInvalidValue(), $test_entity->name->value, 'Violation contains invalid value.');
+    $this->assertEqual($violation->getMessage(), t('This value is too long. It should have %limit characters or less.', array('%limit' => '32')));
+  }
+
+}
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index b6d37f0..e935805 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -2302,6 +2302,11 @@ function system_data_type_info() {
       'description' => t('An entity field referencing a language.'),
       'class' => '\Drupal\Core\Entity\Field\Type\LanguageItem',
       'list class' => '\Drupal\Core\Entity\Field\Type\Field',
+      'constraints' => array(
+        'ComplexData' => array(
+          'value' => array('Length' => array('max' => 12)),
+        ),
+      ),
     ),
     'entity_reference_field' => array(
       'label' => t('Entity reference field item'),
diff --git a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php
index 8c5e23f..1d2b879 100644
--- a/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php
+++ b/core/modules/system/tests/modules/entity_test/lib/Drupal/entity_test/EntityTestStorageController.php
@@ -41,6 +41,9 @@ public function baseFieldDefinitions() {
       'label' => t('UUID'),
       'description' => t('The UUID of the test entity.'),
       'type' => 'string_field',
+      'property_constraints' => array(
+        'value' => array('Length' => array('max' => 128)),
+      ),
     );
     $fields['langcode'] = array(
       'label' => t('Language code'),
@@ -52,11 +55,16 @@ public function baseFieldDefinitions() {
       'description' => t('The name of the test entity.'),
       'type' => 'string_field',
       'translatable' => TRUE,
+      'property_constraints' => array(
+        'value' => array('Length' => array('max' => 32)),
+      ),
     );
     $fields['type'] = array(
       'label' => t('Type'),
       'description' => t('The bundle of the test entity.'),
       'type' => 'string_field',
+      'required' => TRUE,
+      // @todo: Add allowed values validation.
     );
     $fields['user_id'] = array(
       'label' => t('User ID'),
