diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index bb4993a172..01a1eeee95 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -277,6 +277,10 @@ config_entity:
     uuid:
       type: string
       label: 'UUID'
+      constraints:
+        Length:
+          max: 128
+          maxMessage: 'UUID: may not be longer than 128 characters.'
     langcode:
       type: string
       label: 'Language code'
diff --git a/core/core.services.yml b/core/core.services.yml
index 40c6a8a6b4..31733463b4 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -331,9 +331,11 @@ services:
     arguments: ['@config.storage', 'config/schema', '', true, '%install_profile%']
   config.typed:
     class: Drupal\Core\Config\TypedConfigManager
-    arguments: ['@config.storage', '@config.storage.schema', '@cache.discovery', '@module_handler']
+    arguments: ['@config.storage', '@config.storage.schema', '@cache.discovery', '@module_handler', '@class_resolver']
     tags:
       - { name: plugin_manager_cache_clear }
+    calls:
+      - [setValidationConstraintManager, ['@validation.constraint']]
   context.handler:
     class: Drupal\Core\Plugin\Context\ContextHandler
     arguments: ['@typed_data_manager']
diff --git a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php
index c507655447..3dd4cac49b 100644
--- a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php
+++ b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php
@@ -2,10 +2,12 @@
 
 namespace Drupal\Core\Config\Schema;
 
+use Drupal\Core\TypedData\ComplexDataInterface;
+
 /**
  * Defines a generic configuration element that contains multiple properties.
  */
-abstract class ArrayElement extends Element implements \IteratorAggregate, TypedConfigInterface {
+abstract class ArrayElement extends Element implements \IteratorAggregate, TypedConfigInterface, ComplexDataInterface {
 
   /**
    * Parsed elements.
@@ -161,4 +163,25 @@ public function isNullable() {
     return isset($this->definition['nullable']) && $this->definition['nullable'] == TRUE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function set($property_name, $value, $notify = TRUE) {
+    $this->value[$property_name] = $value;
+    // Config schema elements to not make use of notifications. Thus, we skip
+    // notifying parents.
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getProperties($include_computed = FALSE) {
+    $properties = array();
+    foreach (array_keys($this->value) as $name) {
+      $properties[$name] = $this->get($name);
+    }
+    return $properties;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Config/Schema/Sequence.php b/core/lib/Drupal/Core/Config/Schema/Sequence.php
index ce8dc1bc0b..afa3ed3bb0 100644
--- a/core/lib/Drupal/Core/Config/Schema/Sequence.php
+++ b/core/lib/Drupal/Core/Config/Schema/Sequence.php
@@ -10,6 +10,12 @@
  *
  * Read https://www.drupal.org/node/1905070 for more details about configuration
  * schema, types and type resolution.
+ *
+ * Note that sequences implement the typed data ComplexDataInterface (via the
+ * parent ArrayElement) rather than the ListInterface. This is, as sequences
+ * have named keys, what is not covered by lists. From the typed data API
+ * perspective sequences are handled as ordered mappings without metadata about
+ * existing properties.
  */
 class Sequence extends ArrayElement {
 
diff --git a/core/lib/Drupal/Core/Config/StorableConfigBase.php b/core/lib/Drupal/Core/Config/StorableConfigBase.php
index 0751e9f59f..1c8fb2c828 100644
--- a/core/lib/Drupal/Core/Config/StorableConfigBase.php
+++ b/core/lib/Drupal/Core/Config/StorableConfigBase.php
@@ -129,7 +129,7 @@ public function getStorage() {
    *
    * @return \Drupal\Core\Config\Schema\Element
    */
-  protected function getSchemaWrapper() {
+  public function getSchemaWrapper() {
     if (!isset($this->schemaWrapper)) {
       $definition = $this->typedConfigManager->getDefinition($this->name);
       $data_definition = $this->typedConfigManager->buildDataDefinition($definition, $this->data);
diff --git a/core/lib/Drupal/Core/Config/TypedConfigManager.php b/core/lib/Drupal/Core/Config/TypedConfigManager.php
index 22795ad6ff..bd5fa99812 100644
--- a/core/lib/Drupal/Core/Config/TypedConfigManager.php
+++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Config\Schema\ConfigSchemaAlterException;
 use Drupal\Core\Config\Schema\ConfigSchemaDiscovery;
+use Drupal\Core\DependencyInjection\ClassResolverInterface;
 use Drupal\Core\Config\Schema\Undefined;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\TypedData\TypedDataManager;
@@ -45,13 +46,18 @@ class TypedConfigManager extends TypedDataManager implements TypedConfigManagerI
    *   The storage object to use for reading schema data
    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
    *   The cache backend to use for caching the definitions.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
+   *   (optional) The class resolver.
    */
-  public function __construct(StorageInterface $configStorage, StorageInterface $schemaStorage, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler) {
+  public function __construct(StorageInterface $configStorage, StorageInterface $schemaStorage, CacheBackendInterface $cache, ModuleHandlerInterface $module_handler, ClassResolverInterface $class_resolver = NULL) {
     $this->configStorage = $configStorage;
     $this->schemaStorage = $schemaStorage;
     $this->setCacheBackend($cache, 'typed_config_definitions');
     $this->alterInfo('config_schema_info');
     $this->moduleHandler = $module_handler;
+    $this->classResolver = $class_resolver ?: \Drupal::service('class_resolver');
   }
 
   /**
@@ -184,6 +190,7 @@ protected function getDefinitionWithReplacements($base_plugin_id, array $replace
     $definition += [
       'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
       'type' => $type,
+      'unwrap_for_canonical_representation' => TRUE,
     ];
     return $definition;
   }
diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
index 5194d85e9a..37cc103875 100644
--- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
+++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
@@ -585,7 +585,7 @@ protected function getFieldItemClass() {
   public function __sleep() {
     // Do not serialize the statically cached property definitions.
     $vars = get_object_vars($this);
-    unset($vars['propertyDefinitions']);
+    unset($vars['propertyDefinitions'], $vars['typedDataManager']);
     return array_keys($vars);
   }
 
diff --git a/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php b/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php
index 0acdba468d..0002497e94 100644
--- a/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php
+++ b/core/lib/Drupal/Core/TypedData/ComplexDataDefinitionBase.php
@@ -42,7 +42,7 @@ public function getMainPropertyName() {
   public function __sleep() {
     // Do not serialize the cached property definitions.
     $vars = get_object_vars($this);
-    unset($vars['propertyDefinitions']);
+    unset($vars['propertyDefinitions'], $vars['typedDataManager']);
     return array_keys($vars);
   }
 
diff --git a/core/lib/Drupal/Core/TypedData/DataDefinition.php b/core/lib/Drupal/Core/TypedData/DataDefinition.php
index 7eec1a90c4..52a4394cd7 100644
--- a/core/lib/Drupal/Core/TypedData/DataDefinition.php
+++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php
@@ -7,6 +7,8 @@
  */
 class DataDefinition implements DataDefinitionInterface, \ArrayAccess {
 
+  use TypedDataTrait;
+
   /**
    * The array holding values for all definition keys.
    *
@@ -258,7 +260,7 @@ public function setSetting($setting_name, $value) {
    */
   public function getConstraints() {
     $constraints = isset($this->definition['constraints']) ? $this->definition['constraints'] : [];
-    $constraints += \Drupal::typedDataManager()->getDefaultConstraints($this);
+    $constraints += $this->getTypedDataManager()->getDefaultConstraints($this);
     return $constraints;
   }
 
@@ -340,4 +342,14 @@ public function toArray() {
     return $this->definition;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function __sleep() {
+    // Never serialize the typed data manager.
+    $vars = get_object_vars($this);
+    unset($vars['typedDataManager']);
+    return array_keys($vars);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/TypedData/TypedDataManager.php b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
index 8c4f265ebd..dbf0d5fec5 100644
--- a/core/lib/Drupal/Core/TypedData/TypedDataManager.php
+++ b/core/lib/Drupal/Core/TypedData/TypedDataManager.php
@@ -117,7 +117,13 @@ public function createDataDefinition($data_type) {
       throw new \InvalidArgumentException("Invalid data type '$data_type' has been given");
     }
     $class = $type_definition['definition_class'];
-    return $class::createFromDataType($data_type);
+    $data_definition = $class::createFromDataType($data_type);
+
+    if (method_exists($data_definition, 'setTypedDataManager')) {
+      $data_definition->setTypedDataManager($this);
+    }
+
+    return $data_definition;
   }
 
   /**
diff --git a/core/modules/config/src/Tests/ConfigEntityListTest.php b/core/modules/config/src/Tests/ConfigEntityListTest.php
index e9950ea424..cb0c730b33 100644
--- a/core/modules/config/src/Tests/ConfigEntityListTest.php
+++ b/core/modules/config/src/Tests/ConfigEntityListTest.php
@@ -29,6 +29,8 @@ protected function setUp() {
     // test.
     \Drupal::entityManager()->getStorage('config_test')->load('override')->delete();
     $this->drupalPlaceBlock('local_actions_block');
+
+    $this->drupalLogin($this->createUser(['view config_test', 'administer config_test']));
   }
 
   /**
@@ -149,7 +151,7 @@ public function testList() {
    */
   public function testListUI() {
     // Log in as an administrative user to access the full menu trail.
-    $this->drupalLogin($this->drupalCreateUser(['access administration pages', 'administer site configuration']));
+    $this->drupalLogin($this->drupalCreateUser(['access administration pages', 'administer site configuration', 'administer config_test']));
 
     // Get the list callback page.
     $this->drupalGet('admin/structure/config_test');
diff --git a/core/modules/config/src/Tests/ConfigEntityTest.php b/core/modules/config/src/Tests/ConfigEntityTest.php
index c0dc97bc7b..78cd31e705 100644
--- a/core/modules/config/src/Tests/ConfigEntityTest.php
+++ b/core/modules/config/src/Tests/ConfigEntityTest.php
@@ -231,7 +231,7 @@ public function testCRUD() {
    * Tests CRUD operations through the UI.
    */
   public function testCRUDUI() {
-    $this->drupalLogin($this->drupalCreateUser(['administer site configuration']));
+    $this->drupalLogin($this->drupalCreateUser(['administer site configuration', 'administer config_test']));
 
     $id = strtolower($this->randomMachineName());
     $label1 = $this->randomMachineName();
diff --git a/core/modules/config/tests/config_test/config/install/config_test.validation.yml b/core/modules/config/tests/config_test/config/install/config_test.validation.yml
new file mode 100644
index 0000000000..37414689fd
--- /dev/null
+++ b/core/modules/config/tests/config_test/config/install/config_test.validation.yml
@@ -0,0 +1,7 @@
+llama: llama
+cat:
+  type: kitten
+  count: 2
+giraffe:
+  hum1: hum1
+  hum2: hum2
diff --git a/core/modules/config/tests/config_test/config/schema/config_test.schema.yml b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml
index e5b75d1430..65787afd7a 100644
--- a/core/modules/config/tests/config_test/config/schema/config_test.schema.yml
+++ b/core/modules/config/tests/config_test/config/schema/config_test.schema.yml
@@ -158,3 +158,33 @@ config_test.foo:
 
 config_test.bar:
   type: config_test.foo
+
+config_test.validation:
+  type: config_object
+  label: 'Configuration type'
+  mapping:
+    llama:
+      type: string
+      constraints:
+        Callback:
+          callback: [\Drupal\config_test\ConfigValidation, validateLlama]
+    cat:
+      type: mapping
+      mapping:
+        type:
+          type: string
+          constraints:
+            Callback:
+              callback: [\Drupal\config_test\ConfigValidation, validateCats]
+        count:
+          type: integer
+          constraints:
+            Callback:
+              callback: [\Drupal\config_test\ConfigValidation, validateCatCount]
+    giraffe:
+      type: sequence
+      sequence:
+        type: string
+        constraints:
+          Callback:
+            callback: [\Drupal\config_test\ConfigValidation, validateGiraffes]
diff --git a/core/modules/config/tests/config_test/config_test.permissions.yml b/core/modules/config/tests/config_test/config_test.permissions.yml
new file mode 100644
index 0000000000..eaf1be61b7
--- /dev/null
+++ b/core/modules/config/tests/config_test/config_test.permissions.yml
@@ -0,0 +1,4 @@
+view config_test:
+  title: 'View ConfigTest entities'
+administer config_test:
+  title: 'Administer ConfigTest entities'
diff --git a/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php b/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php
index 88896f0b96..4df889f3d1 100644
--- a/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php
+++ b/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Entity\EntityAccessControlHandler;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\node\Plugin\views\filter\Access;
 
 /**
  * Defines the access control handler for the config_test entity type.
@@ -18,14 +19,17 @@ class ConfigTestAccessControlHandler extends EntityAccessControlHandler {
    * {@inheritdoc}
    */
   public function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
-    return AccessResult::allowed();
+    if ($operation === 'view') {
+      return AccessResult::allowedIfHasPermission($account, 'view config_test');
+    }
+    return AccessResult::allowedIfHasPermission($account, 'administer config_test');
   }
 
   /**
    * {@inheritdoc}
    */
   protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
-    return AccessResult::allowed();
+    return AccessResult::allowedIfHasPermission($account, 'administer config_test');
   }
 
 }
diff --git a/core/modules/config/tests/config_test/src/ConfigValidation.php b/core/modules/config/tests/config_test/src/ConfigValidation.php
new file mode 100644
index 0000000000..8cb57b8eed
--- /dev/null
+++ b/core/modules/config/tests/config_test/src/ConfigValidation.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\config_test;
+
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
+
+/**
+ * Provides a collection of validation callbacks for testing purposes.
+ */
+class ConfigValidation {
+
+  /**
+   * Validates a llama.
+   *
+   * @param string $object
+   *   The string to validate.
+   * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
+   *   The validation execution context.
+   */
+  public static function validateLlama($object, ExecutionContextInterface $context) {
+    if (!in_array($object, ['llama', 'alpaca', 'guanaco', 'vicuña'], TRUE)) {
+      $context->addViolation('no valid llama');
+    }
+  }
+
+  /**
+   * Validates cats.
+   *
+   * @param string $object
+   *   The string to validate.
+   * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
+   *   The validation execution context.
+   */
+  public static function validateCats($object, ExecutionContextInterface $context) {
+    if (!in_array($object, ['kitten', 'cats', 'nyans'])) {
+      $context->addViolation('no valid cat');
+    }
+  }
+
+  /**
+   * Validates a number.
+   *
+   * @param string $object
+   *   The string to validate.
+   * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
+   *   The validation execution context.
+   */
+  public static function validateCatCount($object, ExecutionContextInterface $context) {
+    if ($object <= 1) {
+      $context->addViolation('no enough cats');
+    }
+  }
+
+  /**
+   * Validates giraffes.
+   *
+   * @param string $object
+   *   The string to validate.
+   * @param \Symfony\Component\Validator\Context\ExecutionContextInterface $context
+   *   The validation execution context.
+   */
+  public static function validateGiraffes($object, ExecutionContextInterface $context) {
+    if (strpos($object, 'hum') !== 0) {
+      $context->addViolation('Giraffes just hum');
+    }
+  }
+
+}
diff --git a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php
index 56484705e1..6e46066616 100644
--- a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php
+++ b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php
@@ -54,7 +54,7 @@ protected function setUp() {
     parent::setUp();
 
     // Create a test user.
-    $web_user = $this->drupalCreateUser(['administer entity_test content', 'administer entity_test fields', 'view test entity']);
+    $web_user = $this->drupalCreateUser(['administer entity_test content', 'administer entity_test fields', 'view test entity', 'administer config_test']);
     $this->drupalLogin($web_user);
   }
 
diff --git a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php
index 010fad7363..634a1bdd31 100644
--- a/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php
+++ b/core/modules/rest/src/Plugin/Deriver/EntityDeriver.php
@@ -2,8 +2,10 @@
 
 namespace Drupal\rest\Plugin\Deriver;
 
+use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\rest\Plugin\rest\resource\ConfigEntityResource;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -88,6 +90,10 @@ public function getDerivativeDefinitions($base_plugin_definition) {
           }
         }
 
+        if ($entity_type instanceof ConfigEntityTypeInterface) {
+          $this->derivatives[$entity_type_id]['class'] = ConfigEntityResource::class;
+        }
+
         $this->derivatives[$entity_type_id] += $base_plugin_definition;
       }
     }
diff --git a/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php
new file mode 100644
index 0000000000..7e16e6e3ac
--- /dev/null
+++ b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\rest\Plugin\rest\resource;
+
+use Drupal\Component\Render\PlainTextOutput;
+use Drupal\Core\Entity\EntityInterface;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+
+/**
+ * Specific config entity resource with special behaviour for validation.
+ */
+class ConfigEntityResource extends EntityResource {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validate(EntityInterface $entity) {
+    // Add a custom config entity resource class in order to provide
+    /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
+
+    $config = \Drupal::configFactory()->getEditable($entity->getConfigDependencyName());
+    $config->setData($entity->toArray());
+
+    $typed_config = $config->getSchemaWrapper();
+
+    $violations = $typed_config->validate();
+
+    if ($violations->count() > 0) {
+      $message = "Unprocessable Entity: validation failed.\n";
+      foreach ($violations as $violation) {
+        // We strip every HTML from the error message to have a nicer to read
+        // message on REST responses.
+        $message .= $violation->getPropertyPath() . ': ' . PlainTextOutput::renderFromHtml($violation->getMessage()) . "\n";
+      }
+      throw new UnprocessableEntityHttpException($message);
+    }
+  }
+
+}
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 9b1d15f005..9111fafccc 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -264,36 +264,38 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
 
     // Overwrite the received properties.
     $entity_keys = $entity->getEntityType()->getKeys();
-    foreach ($entity->_restSubmittedFields as $field_name) {
-      $field = $entity->get($field_name);
-
-      // Entity key fields need special treatment: together they uniquely
-      // identify the entity. Therefore it does not make sense to modify any of
-      // them. However, rather than throwing an error, we just ignore them as
-      // long as their specified values match their current values.
-      if (in_array($field_name, $entity_keys, TRUE)) {
-        // @todo Work around the wrong assumption that entity keys need special
-        // treatment, when only read-only fields need it.
-        // This will be fixed in https://www.drupal.org/node/2824851.
-        if ($entity->getEntityTypeId() == 'comment' && $field_name == 'status' && !$original_entity->get($field_name)->access('edit')) {
-          throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
+    if ($entity instanceof FieldableEntityInterface) {
+      foreach ($entity->_restSubmittedFields as $field_name) {
+        $field = $entity->get($field_name);
+
+        // Entity key fields need special treatment: together they uniquely
+        // identify the entity. Therefore it does not make sense to modify any of
+        // them. However, rather than throwing an error, we just ignore them as
+        // long as their specified values match their current values.
+        if (in_array($field_name, $entity_keys, TRUE)) {
+          // @todo Work around the wrong assumption that entity keys need special
+          // treatment, when only read-only fields need it.
+          // This will be fixed in https://www.drupal.org/node/2824851.
+          if ($entity->getEntityTypeId() == 'comment' && $field_name == 'status' && !$original_entity->get($field_name)->access('edit')) {
+            throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
+          }
+
+          // Unchanged values for entity keys don't need access checking.
+          if ($this->getCastedValueFromFieldItemList($original_entity->get($field_name)) === $this->getCastedValueFromFieldItemList($entity->get($field_name))) {
+            continue;
+          }
+          // It is not possible to set the language to NULL as it is automatically
+          // re-initialized. As it must not be empty, skip it if it is.
+          elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) {
+            continue;
+          }
         }
 
-        // Unchanged values for entity keys don't need access checking.
-        if ($this->getCastedValueFromFieldItemList($original_entity->get($field_name)) === $this->getCastedValueFromFieldItemList($entity->get($field_name))) {
-          continue;
-        }
-        // It is not possible to set the language to NULL as it is automatically
-        // re-initialized. As it must not be empty, skip it if it is.
-        elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) {
-          continue;
+        if (!$original_entity->get($field_name)->access('edit')) {
+          throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
         }
+        $original_entity->set($field_name, $field->getValue());
       }
-
-      if (!$original_entity->get($field_name)->access('edit')) {
-        throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
-      }
-      $original_entity->set($field_name, $field->getValue());
     }
 
     // Validate the received data before saving.
@@ -388,20 +390,6 @@ protected function getBaseRoute($canonical_path, $method) {
   }
 
   /**
-   * {@inheritdoc}
-   */
-  public function availableMethods() {
-    $methods = parent::availableMethods();
-    if ($this->isConfigEntityResource()) {
-      // Currently only GET is supported for Config Entities.
-      // @todo Remove when supported https://www.drupal.org/node/2300677
-      $unsupported_methods = ['POST', 'PUT', 'DELETE', 'PATCH'];
-      $methods = array_diff($methods, $unsupported_methods);
-    }
-    return $methods;
-  }
-
-  /**
    * Checks if this resource is for a Config Entity.
    *
    * @return bool
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php
index 7bf8e824e1..cb286dc0b4 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php
@@ -3,6 +3,7 @@
 namespace Drupal\rest\Plugin\rest\resource;
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 
 /**
@@ -25,9 +26,11 @@ protected function checkEditFieldAccess(EntityInterface $entity) {
     // Only check 'edit' permissions for fields that were actually submitted by
     // the user. Field access makes no difference between 'create' and 'update',
     // so the 'edit' operation is used here.
-    foreach ($entity->_restSubmittedFields as $key => $field_name) {
-      if (!$entity->get($field_name)->access('edit')) {
-        throw new AccessDeniedHttpException("Access denied on creating field '$field_name'.");
+    if ($entity instanceof FieldableEntityInterface) {
+      foreach ($entity->_restSubmittedFields as $key => $field_name) {
+        if (!$entity->get($field_name)->access('edit')) {
+          throw new AccessDeniedHttpException("Access denied on creating field '$field_name'.");
+        }
       }
     }
   }
diff --git a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
index fcd9979a11..0cffb9c1f4 100644
--- a/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
+++ b/core/modules/rest/tests/modules/config_test_rest/config_test_rest.module
@@ -26,5 +26,8 @@ function config_test_rest_config_test_access(EntityInterface $entity, $operation
   // Add permission, so that EntityResourceTestBase's scenarios can test access
   // being denied. By default, all access is always allowed for the config_test
   // config entity.
-  return AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
+  if ($operation === 'view') {
+    return AccessResult::forbiddenIf(!$account->hasPermission('view config_test'))->cachePerPermissions();
+  }
+  return AccessResult::neutral();
 }
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php
index 9fe073b097..1ec31a5ea3 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php
@@ -22,11 +22,31 @@
    */
   protected $entity;
 
+  protected $counter = 0;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $configEntityWithModifyingTest = TRUE;
+
   /**
    * {@inheritdoc}
    */
   protected function setUpAuthorization($method) {
-    $this->grantPermissionsToTestedRole(['view config_test']);
+    switch ($method) {
+      case 'GET':
+        $this->grantPermissionsToTestedRole(['view config_test']);
+        break;
+      case 'POST':
+        $this->grantPermissionsToTestedRole(['administer config_test']);
+        break;
+      case 'PATCH':
+        $this->grantPermissionsToTestedRole(['administer config_test']);
+        break;
+      case 'DELETE':
+        $this->grantPermissionsToTestedRole(['administer config_test']);
+        break;
+    }
   }
 
   /**
@@ -67,7 +87,33 @@ protected function getExpectedNormalizedEntity() {
    * {@inheritdoc}
    */
   protected function getNormalizedPostEntity() {
-    // @todo Update in https://www.drupal.org/node/2300677.
+    $this->counter++;
+    return [
+      'id' => 'llama' . (string) $this->counter,
+      'label' => 'Llamam',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function makeNormalizationInvalid(array $normalization) {
+    $normalization['label'] = ['foo', 'bar'];
+    return $normalization;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedUnauthorizedAccessMessage($method) {
+    if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
+      return parent::getExpectedUnauthorizedAccessMessage($method);
+    }
+
+    if ($method === 'GET') {
+      return 'You are not authorized to view this config_test entity.';
+    }
+    return "The 'administer config_test' permission is required.";
   }
 
 }
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 81baf9ee17..9d4fbf799c 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -75,7 +75,7 @@
    *
    * @var string[]
    */
-  protected static $patchProtectedFieldNames;
+  protected static $patchProtectedFieldNames = [];
 
   /**
    * Optionally specify which field is the 'label' field. Some entities specify
@@ -131,6 +131,13 @@
   public static $modules = ['rest_test', 'text'];
 
   /**
+   * Flag to indicate a test which can modify config entities.
+   *
+   * @var bool
+   */
+  protected $configEntityWithModifyingTest = FALSE;
+
+  /**
    * Provides an entity resource.
    */
   protected function provisionEntityResource() {
@@ -585,7 +592,7 @@ protected static function castToString(array $normalization) {
    */
   public function testPost() {
     // @todo Remove this in https://www.drupal.org/node/2300677.
-    if ($this->entity instanceof ConfigEntityInterface) {
+    if ($this->entity instanceof ConfigEntityInterface && !$this->configEntityWithModifyingTest) {
       $this->assertTrue(TRUE, 'POSTing config entities is not yet supported.');
       return;
     }
@@ -598,7 +605,12 @@ public function testPost() {
     $parseable_valid_request_body   = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format);
     $parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format);
     $parseable_invalid_request_body   = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPostEntity()), static::$format);
-    $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [$this->randomMachineName(129)]], static::$format);
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [$this->randomMachineName(129)]], static::$format);
+    }
+    else {
+      $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => $this->randomMachineName(129)], static::$format);
+    }
     $parseable_invalid_request_body_3 = $this->serializer->encode($this->getNormalizedPostEntity() + ['field_rest_test' => [['value' => $this->randomString()]]], static::$format);
 
     // The URL and Guzzle request options that will be used in this test. The
@@ -684,12 +696,13 @@ public function testPost() {
     $this->setUpAuthorization('POST');
 
 
-    // DX: 422 when invalid entity: multiple values sent for single-value field.
-    $response = $this->request('POST', $url, $request_options);
-    $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
-    $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
-
+    if ($this->entity instanceof FieldableEntityInterface) {
+      // DX: 422 when invalid entity: multiple values sent for single-value field.
+      $response = $this->request('POST', $url, $request_options);
+      $label_field = $this->entity->getEntityType() ->hasKey('label') ? $this->entity->getEntityType() ->getKey('label') : static::$labelFieldName;
+      $label_field_capitalized = $this->entity->getFieldDefinition($label_field) ->getLabel();
+      $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
+    }
 
     $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
 
@@ -698,16 +711,23 @@ public function testPost() {
     // @todo Fix this in https://www.drupal.org/node/2149851.
     if ($this->entity->getEntityType()->hasKey('uuid')) {
       $response = $this->request('POST', $url, $request_options);
-      $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response);
+      if ($this->entity instanceof FieldableEntityInterface) {
+        $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response);
+      }
+      else {
+        $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid: UUID: may not be longer than 128 characters.\n", $response);
+      }
     }
 
 
-    $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
 
 
-    // DX: 403 when entity contains field without 'edit' access.
-    $response = $this->request('POST', $url, $request_options);
-    $this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'.", $response);
+      // DX: 403 when entity contains field without 'edit' access.
+      $response = $this->request('POST', $url, $request_options);
+      $this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'.", $response);
+    }
 
 
     $request_options[RequestOptions::BODY] = $parseable_valid_request_body;
@@ -774,7 +794,7 @@ public function testPost() {
    */
   public function testPatch() {
     // @todo Remove this in https://www.drupal.org/node/2300677.
-    if ($this->entity instanceof ConfigEntityInterface) {
+    if ($this->entity instanceof ConfigEntityInterface && !$this->configEntityWithModifyingTest) {
       $this->assertTrue(TRUE, 'PATCHing config entities is not yet supported.');
       return;
     }
@@ -885,18 +905,22 @@ public function testPatch() {
 
 
     // DX: 422 when invalid entity: multiple values sent for single-value field.
-    $response = $this->request('PATCH', $url, $request_options);
-    $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
-    $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
-    $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $response = $this->request('PATCH', $url, $request_options);
+      $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
+      $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
+      $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
+    }
 
 
     $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
 
 
     // DX: 403 when entity contains field without 'edit' access.
-    $response = $this->request('PATCH', $url, $request_options);
-    $this->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response);
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $response = $this->request('PATCH', $url, $request_options);
+      $this->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response);
+    }
 
 
     // DX: 403 when sending PATCH request with read-only fields.
@@ -943,10 +967,12 @@ public function testPatch() {
     $response = $this->request('PATCH', $url, $request_options);
     $this->assertResourceResponse(200, FALSE, $response);
     $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
-    // Ensure that fields do not get deleted if they're not present in the PATCH
-    // request. Test this using the configurable field that we added, but which
-    // is not sent in the PATCH request.
-    $this->assertSame('All the faith he had had had had no effect on the outcome of his life.', $this->entityStorage->loadUnchanged($this->entity->id())->get('field_rest_test')->value);
+    if ($this->entity instanceof FieldableEntityInterface) {
+      // Ensure that fields do not get deleted if they're not present in the PATCH
+      // request. Test this using the configurable field that we added, but which
+      // is not sent in the PATCH request.
+      $this->assertSame('All the faith he had had had had no effect on the outcome of his life.', $this->entityStorage->loadUnchanged($this->entity->id())->get('field_rest_test')->value);
+    }
 
 
     $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
@@ -973,7 +999,7 @@ public function testPatch() {
    */
   public function testDelete() {
     // @todo Remove this in https://www.drupal.org/node/2300677.
-    if ($this->entity instanceof ConfigEntityInterface) {
+    if ($this->entity instanceof ConfigEntityInterface && !$this->configEntityWithModifyingTest) {
       $this->assertTrue(TRUE, 'DELETEing config entities is not yet supported.');
       return;
     }
diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml
index c23ed7e0ae..dce4e975bc 100644
--- a/core/modules/system/config/schema/system.schema.yml
+++ b/core/modules/system/config/schema/system.schema.yml
@@ -7,6 +7,8 @@ system.site:
     uuid:
       type: string
       label: 'Site UUID'
+      constraints:
+        NotNull: []
     name:
       type: label
       label: 'Site name'
diff --git a/core/modules/views/tests/src/Kernel/TestViewsTest.php b/core/modules/views/tests/src/Kernel/TestViewsTest.php
index f9aa673421..dcd1859be3 100644
--- a/core/modules/views/tests/src/Kernel/TestViewsTest.php
+++ b/core/modules/views/tests/src/Kernel/TestViewsTest.php
@@ -34,7 +34,8 @@ public function testDefaultConfig() {
       \Drupal::service('config.storage'),
       new TestInstallStorage(InstallStorage::CONFIG_SCHEMA_DIRECTORY),
       \Drupal::service('cache.discovery'),
-      \Drupal::service('module_handler')
+      \Drupal::service('module_handler'),
+      \Drupal::service('class_resolver')
     );
 
     // Create a configuration storage with access to default configuration in
diff --git a/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php b/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php
new file mode 100644
index 0000000000..f8042dfcfa
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace Drupal\KernelTests\Config;
+
+use Drupal\Core\Config\Schema\SequenceDataDefinition;
+use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\Type\IntegerInterface;
+use Drupal\Core\TypedData\Type\StringInterface;
+use Drupal\KernelTests\KernelTestBase;
+use Symfony\Component\Validator\ConstraintViolationListInterface;
+
+/**
+ * Tests config validation mechanism.
+ *
+ * @group Config
+ */
+class TypedConfigTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['config_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installConfig('config_test');
+  }
+
+  /**
+   * Verifies that the Typed Data API is implemented correctly.
+   */
+  public function testTypedDataAPI() {
+    /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager */
+    $typed_config_manager = \Drupal::service('config.typed');
+    /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */
+    $typed_config = $typed_config_manager->get('config_test.validation');
+
+    // Test a primitive.
+    $string_data = $typed_config->get('llama');
+    $this->assertInstanceOf(StringInterface::class, $string_data);
+    $this->assertEquals('llama', $string_data->getValue());
+
+    // Test complex data.
+    $mapping = $typed_config->get('cat');
+    /** @var \Drupal\Core\TypedData\ComplexDataInterface $mapping */
+    $this->assertInstanceOf(ComplexDataInterface::class, $mapping);
+    $this->assertInstanceOf(StringInterface::class, $mapping->get('type'));
+    $this->assertEquals('kitten', $mapping->get('type')->getValue());
+    $this->assertInstanceOf(IntegerInterface::class, $mapping->get('count'));
+    $this->assertEquals(2, $mapping->get('count')->getValue());
+    // Verify the item metadata is available.
+    $this->assertInstanceOf(ComplexDataDefinitionInterface::class, $mapping->getDataDefinition());
+    $this->assertArrayHasKey('type', $mapping->getProperties());
+    $this->assertArrayHasKey('count', $mapping->getProperties());
+
+    // Test accessing sequences.
+    $sequence = $typed_config->get('giraffe');
+    /** @var \Drupal\Core\TypedData\ListInterface $sequence */
+    $this->assertInstanceOf(ComplexDataInterface::class, $sequence);
+    $this->assertInstanceOf(StringInterface::class, $sequence->get('hum1'));
+    $this->assertEquals('hum1', $sequence->get('hum1')->getValue());
+    $this->assertEquals('hum2', $sequence->get('hum2')->getValue());
+    $this->assertEquals(2, count($sequence->getIterator()));
+    // Verify the item metadata is available.
+    $this->assertInstanceOf(SequenceDataDefinition::class, $sequence->getDataDefinition());
+  }
+
+  /**
+   * Tests config validation via the typed data api.
+   */
+  public function testSimpleConfigValidation() {
+    $config = \Drupal::configFactory()->getEditable('config_test.validation');
+    /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager */
+    $typed_config_manager = \Drupal::service('config.typed');
+    /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */
+    $typed_config = $typed_config_manager->get('config_test.validation');
+
+    $result = $typed_config->validate();
+    $this->assertInstanceOf(ConstraintViolationListInterface::class, $result);
+    $this->assertEmpty($result);
+
+    // Test constraints on primitive types.
+    $config->set('llama', 'elephant');
+    $config->save();
+
+    $typed_config = $typed_config_manager->get('config_test.validation');
+    $result = $typed_config->validate();
+    // Its not a valid llama anymore.
+    $this->assertCount(1, $result);
+    $this->assertEquals('no valid llama', $result->get(0)->getMessage());
+
+    // Test constraints on mapping.
+    $config->set('llama', 'llama');
+    $config->set('cat.type', 'nyans');
+    $config->save();
+
+    $typed_config = $typed_config_manager->get('config_test.validation');
+    $result = $typed_config->validate();
+    $this->assertEmpty($result);
+
+    // Test constrains on nested mapping.
+    $config->set('cat.type', 'miaus');
+    $config->save();
+
+    $typed_config = $typed_config_manager->get('config_test.validation');
+    $result = $typed_config->validate();
+    $this->assertCount(1, $result);
+    $this->assertEquals('no valid cat', $result->get(0)->getMessage());
+
+    // Test constrains on sequences.
+    $config->set('cat.type', 'nyans');
+    $config->set('giraffe', ['muh', 'hum2']);
+    $config->save();
+
+    $typed_config = $typed_config_manager->get('config_test.validation');
+    $result = $typed_config->validate();
+    $this->assertCount(1, $result);
+    $this->assertEquals('Giraffes just hum', $result->get(0)->getMessage());
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
index 96bd18c0a2..7145520b72 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
@@ -47,6 +47,7 @@ public function testSchemaMapping() {
     $expected['class'] = Undefined::class;
     $expected['type'] = 'undefined';
     $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
     $this->assertEqual($definition, $expected, 'Retrieved the right metadata for nonexistent configuration.');
 
     // Configuration file without schema will return Undefined as well.
@@ -67,6 +68,7 @@ public function testSchemaMapping() {
     $expected['mapping']['testlist'] = ['label' => 'Test list'];
     $expected['type'] = 'config_schema_test.someschema';
     $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
     $this->assertEqual($definition, $expected, 'Retrieved the right metadata for configuration with only some schema.');
 
     // Check type detection on elements with undefined types.
@@ -77,6 +79,7 @@ public function testSchemaMapping() {
     $expected['class'] = Undefined::class;
     $expected['type'] = 'undefined';
     $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
     $this->assertEqual($definition, $expected, 'Automatic type detected for a scalar is undefined.');
     $definition = $config->get('testlist')->getDataDefinition()->toArray();
     $expected = [];
@@ -84,6 +87,7 @@ public function testSchemaMapping() {
     $expected['class'] = Undefined::class;
     $expected['type'] = 'undefined';
     $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
     $this->assertEqual($definition, $expected, 'Automatic type detected for a list is undefined.');
     $definition = $config->get('testnoschema')->getDataDefinition()->toArray();
     $expected = [];
@@ -91,6 +95,7 @@ public function testSchemaMapping() {
     $expected['class'] = Undefined::class;
     $expected['type'] = 'undefined';
     $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
     $this->assertEqual($definition, $expected, 'Automatic type detected for an undefined integer is undefined.');
 
     // Simple case, straight metadata.
@@ -109,6 +114,7 @@ public function testSchemaMapping() {
     $expected['mapping']['_core']['type'] = '_core_config_info';
     $expected['type'] = 'system.maintenance';
     $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
     $this->assertEqual($definition, $expected, 'Retrieved the right metadata for system.maintenance');
 
     // Mixed schema with ignore elements.
@@ -139,6 +145,7 @@ public function testSchemaMapping() {
       'type' => 'integer',
     ];
     $expected['type'] = 'config_schema_test.ignore';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
 
     $this->assertEqual($definition, $expected);
 
@@ -149,6 +156,7 @@ public function testSchemaMapping() {
     $expected['label'] = 'Irrelevant';
     $expected['class'] = Ignore::class;
     $expected['definition_class'] = '\Drupal\Core\TypedData\DataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
     $this->assertEqual($definition, $expected);
     $definition = \Drupal::service('config.typed')->get('config_schema_test.ignore')->get('indescribable')->getDataDefinition()->toArray();
     $expected['label'] = 'Indescribable';
@@ -160,9 +168,16 @@ public function testSchemaMapping() {
     $expected['label'] = 'Image style';
     $expected['class'] = Mapping::class;
     $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
     $expected['mapping']['name']['type'] = 'string';
     $expected['mapping']['uuid']['type'] = 'string';
     $expected['mapping']['uuid']['label'] = 'UUID';
+    $expected['mapping']['uuid']['constraints'] = [
+      'Length' => [
+        'max' => 128,
+        'maxMessage' => 'UUID: may not be longer than 128 characters.',
+      ],
+    ];
     $expected['mapping']['langcode']['type'] = 'string';
     $expected['mapping']['langcode']['label'] = 'Language code';
     $expected['mapping']['status']['type'] = 'boolean';
@@ -193,6 +208,7 @@ public function testSchemaMapping() {
     $expected['label'] = 'Image scale';
     $expected['class'] = Mapping::class;
     $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
     $expected['mapping']['width']['type'] = 'integer';
     $expected['mapping']['width']['label'] = 'Width';
     $expected['mapping']['height']['type'] = 'integer';
@@ -220,6 +236,7 @@ public function testSchemaMapping() {
     $expected['label'] = 'Mapping';
     $expected['class'] = Mapping::class;
     $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
     $expected['mapping'] = [
       'integer' => ['type' => 'integer'],
       'string' => ['type' => 'string'],
@@ -241,6 +258,7 @@ public function testSchemaMapping() {
     $expected['mapping']['testdescription']['label'] = 'Description';
     $expected['type'] = 'config_schema_test.someschema.somemodule.*.*';
     $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
 
     $this->assertEqual($definition, $expected, 'Retrieved the right metadata for config_schema_test.someschema.somemodule.section_one.subsection');
 
@@ -263,6 +281,7 @@ public function testSchemaMappingWithParents() {
       'label' => 'Test item nested one level',
       'class' => StringData::class,
       'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
+      'unwrap_for_canonical_representation' => TRUE,
     ];
     $this->assertEqual($definition, $expected);
 
@@ -274,6 +293,7 @@ public function testSchemaMappingWithParents() {
       'label' => 'Test item nested two levels',
       'class' => StringData::class,
       'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
+      'unwrap_for_canonical_representation' => TRUE,
     ];
     $this->assertEqual($definition, $expected);
 
@@ -285,6 +305,7 @@ public function testSchemaMappingWithParents() {
       'label' => 'Test item nested three levels',
       'class' => StringData::class,
       'definition_class' => '\Drupal\Core\TypedData\DataDefinition',
+      'unwrap_for_canonical_representation' => TRUE,
     ];
     $this->assertEqual($definition, $expected);
   }
@@ -475,6 +496,7 @@ public function testSchemaFallback() {
     $expected['label'] = 'Schema wildcard fallback test';
     $expected['class'] = Mapping::class;
     $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
+    $expected['unwrap_for_canonical_representation'] = TRUE;
     $expected['mapping']['langcode']['type'] = 'string';
     $expected['mapping']['langcode']['label'] = 'Language code';
     $expected['mapping']['_core']['type'] = '_core_config_info';
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php
index 9d5095d149..5e755d9584 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityTypedDataDefinitionTest.php
@@ -101,8 +101,8 @@ public function testEntities() {
 
     // Test that the definition factory creates the right definitions for all
     // entity data types variants.
-    $this->assertEqual($this->typedDataManager->createDataDefinition('entity'), EntityDataDefinition::create());
-    $this->assertEqual($this->typedDataManager->createDataDefinition('entity:node'), EntityDataDefinition::create('node'));
+    $this->assertEqual(serialize($this->typedDataManager->createDataDefinition('entity')), serialize(EntityDataDefinition::create()));
+    $this->assertEqual(serialize($this->typedDataManager->createDataDefinition('entity:node')), serialize(EntityDataDefinition::create('node')));
 
     // Config entities don't support typed data.
     $entity_definition = EntityDataDefinition::create('node_type');
@@ -123,7 +123,7 @@ public function testEntityReferences() {
     // Test that the definition factory creates the right definition object.
     $reference_definition2 = $this->typedDataManager->createDataDefinition('entity_reference');
     $this->assertTrue($reference_definition2 instanceof DataReferenceDefinitionInterface);
-    $this->assertEqual($reference_definition2, $reference_definition);
+    $this->assertEqual(serialize($reference_definition2), serialize($reference_definition));
   }
 
 }
diff --git a/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataDefinitionTest.php b/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataDefinitionTest.php
index 8aeab575e9..ad266c2852 100644
--- a/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataDefinitionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataDefinitionTest.php
@@ -77,7 +77,7 @@ public function testMaps() {
     $map_definition2->setPropertyDefinition('one', DataDefinition::create('string'))
       ->setPropertyDefinition('two', DataDefinition::create('string'))
       ->setPropertyDefinition('three', DataDefinition::create('string'));
-    $this->assertEqual($map_definition, $map_definition2);
+    $this->assertEqual(serialize($map_definition), serialize($map_definition2));
   }
 
   /**
@@ -93,7 +93,7 @@ public function testDataReferences() {
     // Test using the definition factory.
     $language_reference_definition2 = $this->typedDataManager->createDataDefinition('language_reference');
     $this->assertTrue($language_reference_definition2 instanceof DataReferenceDefinitionInterface);
-    $this->assertEqual($language_reference_definition, $language_reference_definition2);
+    $this->assertEqual(serialize($language_reference_definition), serialize($language_reference_definition2));
   }
 
 }
