diff --git a/core/lib/Drupal/Core/Entity/ContentEntityType.php b/core/lib/Drupal/Core/Entity/ContentEntityType.php
index 0e26c3bb51..c4033b5dbb 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityType.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityType.php
@@ -14,6 +14,11 @@ class ContentEntityType extends EntityType implements ContentEntityTypeInterface
    */
   protected $revision_metadata_keys = [];
 
+  /**
+   * {@inheritdoc}
+   */
+  protected $supports_validation = TRUE;
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php
index 5f6589a6b4..1132a7a6eb 100644
--- a/core/lib/Drupal/Core/Entity/EntityType.php
+++ b/core/lib/Drupal/Core/Entity/EntityType.php
@@ -277,6 +277,13 @@ class EntityType extends PluginDefinition implements EntityTypeInterface {
    */
   protected $additional = [];
 
+  /**
+   * Entity type supports validation.
+   *
+   * @var bool
+   */
+  protected $supports_validation = FALSE;
+
   /**
    * Constructs a new EntityType.
    *
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_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..0aeb6e0d63 100644
--- a/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php
+++ b/core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php
@@ -18,14 +18,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/Entity/ConfigTest.php b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php
index 1bd1d8dee9..4f84ecfe1f 100644
--- a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php
+++ b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php
@@ -22,6 +22,7 @@
  *     },
  *     "access" = "Drupal\config_test\ConfigTestAccessControlHandler"
  *   },
+ *   supports_validation = TRUE,
  *   config_prefix = "dynamic",
  *   entity_keys = {
  *     "id" = "id",
diff --git a/core/modules/content_moderation/src/Entity/ContentModerationState.php b/core/modules/content_moderation/src/Entity/ContentModerationState.php
index e54fbbe2a8..f2d74b81ce 100644
--- a/core/modules/content_moderation/src/Entity/ContentModerationState.php
+++ b/core/modules/content_moderation/src/Entity/ContentModerationState.php
@@ -31,6 +31,7 @@
  *   data_table = "content_moderation_state_field_data",
  *   revision_data_table = "content_moderation_state_field_revision",
  *   translatable = TRUE,
+ *   supports_validation = FALSE,
  *   entity_keys = {
  *     "id" = "id",
  *     "revision" = "revision_id",
diff --git a/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php b/core/modules/field/tests/src/Functional/EntityReference/EntityReferenceIntegrationTest.php
index 56484705e1..14b2cae406 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', 'view config_test', '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 a74e8b2a61..8940c910b6 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..91d4e89b21
--- /dev/null
+++ b/core/modules/rest/src/Plugin/rest/resource/ConfigEntityResource.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\rest\Plugin\rest\resource;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Specific config entity resource with special behaviour for validation.
+ */
+class ConfigEntityResource extends EntityResource {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validate(EntityInterface $entity) {
+    // Use typed config to validate the validate the config entity.
+    /** @var \Drupal\Core\Config\TypedConfigManagerInterface $type_config_manager */
+    $type_config_manager = \Drupal::service('config.typed');
+    $typed_config = $type_config_manager->createFromNameAndData($entity->getConfigDependencyName(), $entity->toArray());
+    $violations = $typed_config->validate();
+
+    $this->processViolations($violations);
+  }
+
+}
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 10874680a6..34c7493c08 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -225,29 +225,31 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
       throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'update'));
     }
 
-    // Overwrite the received fields.
-    foreach ($entity->_restSubmittedFields as $field_name) {
-      $field = $entity->get($field_name);
-      $original_field = $original_entity->get($field_name);
-
-      // If the user has access to view the field, we need to check update
-      // access regardless of the field value to avoid information disclosure.
-      // (Otherwise the user may try PATCHing with value after value, until they
-      // send the current value for the field, and then they won't get a 403
-      // response anymore, which indicates that the value they sent in the PATCH
-      // request body matches the current value.)
-      if (!$original_field->access('view')) {
-        if (!$original_field->access('edit')) {
+    if ($entity instanceof FieldableEntityInterface) {
+      // Overwrite the received fields.
+      foreach ($entity->_restSubmittedFields as $field_name) {
+        $field = $entity->get($field_name);
+        $original_field = $original_entity->get($field_name);
+
+        // If the user has access to view the field, we need to check update
+        // access regardless of the field value to avoid information disclosure.
+        // (Otherwise the user may try PATCHing with value after value, until
+        // they send the current value for the field, and then they won't get a
+        // 403 response anymore, which indicates that the value they sent in the
+        // PATCH request body matches the current value.)
+        if (!$original_field->access('view')) {
+          if (!$original_field->access('edit')) {
+            throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
+          }
+        }
+        // Check access for all received fields, but only if they are being
+        // changed. The bundle of an entity, for example, must be provided for
+        // denormalization to succeed, but it may not be changed.
+        elseif (!$original_field->equals($field) && !$original_field->access('edit')) {
           throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
         }
+        $original_entity->set($field_name, $field->getValue());
       }
-      // Check access for all received fields, but only if they are being
-      // changed. The bundle of an entity, for example, must be provided for
-      // denormalization to succeed, but it may not be changed.
-      elseif (!$original_field->equals($field) && !$original_field->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.
@@ -346,25 +348,14 @@ protected function getBaseRoute($canonical_path, $method) {
    */
   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
+    // Without validation, it's impossible to support creation or modification.
+    if (!$this->entityType->get('supports_validation')) {
       $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
-   *   TRUE if the entity is a Config Entity, FALSE otherwise.
-   */
-  protected function isConfigEntityResource() {
-    return $this->entityType instanceof ConfigEntityType;
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php
index 7bf8e824e1..4480a85bcb 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;
 
 /**
@@ -22,6 +23,10 @@
    *   field.
    */
   protected function checkEditFieldAccess(EntityInterface $entity) {
+    if (!$entity instanceof FieldableEntityInterface) {
+      return;
+    }
+
     // 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.
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
index 09b4b64bae..de1bb77f03 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
@@ -33,6 +33,19 @@ protected function validate(EntityInterface $entity) {
     // changes.
     $violations->filterByFieldAccess();
 
+    $this->processViolations($violations);
+  }
+
+  /**
+   * Processes violations and creates a helpful exception message.
+   *
+   * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
+   *   The entity constraint violations to process.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
+   *   Throws a HTTP exception when the validation fails.
+   */
+  protected function processViolations($violations) {
     if ($violations->count() > 0) {
       $message = "Unprocessable Entity: validation failed.\n";
       foreach ($violations as $violation) {
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..65af2c5fef 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/ConfigTest/ConfigTestResourceTestBase.php
@@ -3,7 +3,9 @@
 namespace Drupal\Tests\rest\Functional\EntityResource\ConfigTest;
 
 use Drupal\config_test\Entity\ConfigTest;
+use Drupal\Core\Url;
 use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
+use GuzzleHttp\RequestOptions;
 
 abstract class ConfigTestResourceTestBase extends EntityResourceTestBase {
 
@@ -22,11 +24,32 @@
    */
   protected $entity;
 
+  /**
+   * Counter used internally to have deterministic IDs.
+   *
+   * @var int
+   */
+  protected $counter = 0;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $firstCreatedEntityId = 'llama1';
+
   /**
    * {@inheritdoc}
    */
   protected function setUpAuthorization($method) {
-    $this->grantPermissionsToTestedRole(['view config_test']);
+    switch ($method) {
+      case 'GET':
+        $this->grantPermissionsToTestedRole(['view config_test']);
+        break;
+      case 'POST':
+      case 'PATCH':
+      case 'DELETE':
+        $this->grantPermissionsToTestedRole(['administer config_test']);
+        break;
+    }
   }
 
   /**
@@ -34,7 +57,7 @@ protected function setUpAuthorization($method) {
    */
   protected function createEntity() {
     $config_test = ConfigTest::create([
-      'id' => 'llama',
+      'id' => 'llama' . (string) $this->counter,
       'label' => 'Llama',
     ]);
     $config_test->save();
@@ -48,7 +71,7 @@ protected function createEntity() {
   protected function getExpectedNormalizedEntity() {
     $normalization = [
       'uuid' => $this->entity->uuid(),
-      'id' => 'llama',
+      'id' => 'llama' . (string) $this->counter,
       'weight' => 0,
       'langcode' => 'en',
       'status' => TRUE,
@@ -63,11 +86,42 @@ protected function getExpectedNormalizedEntity() {
     return $normalization;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {
+    parent::assertNormalizationEdgeCases($method, $url, $request_options);
+
+    $normalization = $this->getNormalizedPostEntity();
+    $normalization['protected_property'] = 'some value';
+    $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
+    $response = $this->request($method, $url, $request_options);
+    $this->assertResourceErrorResponse(422, '', $response);
+  }
+
   /**
    * {@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 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 72589bd3b8..4daed7dfa1 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -6,7 +6,6 @@
 use Drupal\Component\Utility\Random;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableResponseInterface;
-use Drupal\Core\Config\Entity\ConfigEntityInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem;
@@ -82,7 +81,7 @@
    *
    * @var string[]
    */
-  protected static $patchProtectedFieldNames;
+  protected static $patchProtectedFieldNames = [];
 
   /**
    * Optionally specify which field is the 'label' field. Some entities specify
@@ -694,9 +693,8 @@ protected static function recursiveKSort(array &$array) {
    * Tests a POST request for an entity, plus edge cases to ensure good DX.
    */
   public function testPost() {
-    // @todo Remove this in https://www.drupal.org/node/2300677.
-    if ($this->entity instanceof ConfigEntityInterface) {
-      $this->assertTrue(TRUE, 'POSTing config entities is not yet supported.');
+    if (!$this->entity->getEntityType()->get('supports_validation')) {
+      $this->assertTrue(TRUE, "This entity type doesn't support POSTing.");
       return;
     }
 
@@ -708,7 +706,14 @@ 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);
+    // The normalized structure is different for fieldable and non-fieldable
+    // entities.
+    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
@@ -796,9 +801,14 @@ public function testPost() {
 
     // 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) {
+      $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);
+    }
+    else {
+      $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nlabel: This value should be of the correct primitive type.\n", $response);
+    }
 
 
     $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
@@ -808,16 +818,25 @@ 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) {
+        // @see \Drupal\Core\Field\Plugin\Field\FieldType\StringItem::getConstraints
+        $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response);
+      }
+      else {
+        // @see \Symfony\Component\Validator\Constraints\Uuid
+        $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid: This is not a valid UUID.\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;
@@ -864,7 +883,7 @@ public function testPost() {
     foreach ($this->getNormalizedPostEntity() as $field_name => $field_normalization) {
       // Some top-level keys in the normalization may not be fields on the
       // entity (for example '_links' and '_embedded' in the HAL normalization).
-      if ($created_entity->hasField($field_name)) {
+      if ($created_entity instanceof FieldableEntityInterface && $created_entity->hasField($field_name)) {
         // Subset, not same, because we can e.g. send just the target_id for the
         // bundle in a POST request; the response will include more properties.
         $this->assertArraySubset(static::castToString($field_normalization), $created_entity->get($field_name)->getValue(), TRUE);
@@ -915,9 +934,8 @@ public function testPost() {
    * Tests a PATCH request for an entity, plus edge cases to ensure good DX.
    */
   public function testPatch() {
-    // @todo Remove this in https://www.drupal.org/node/2300677.
-    if ($this->entity instanceof ConfigEntityInterface) {
-      $this->assertTrue(TRUE, 'PATCHing config entities is not yet supported.');
+    if (!$this->entity->getEntityType()->get('supports_validation')) {
+      $this->assertTrue(TRUE, "This entity type doesn't support PATCHing.");
       return;
     }
 
@@ -1028,18 +1046,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);
+    }
 
 
     $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
@@ -1097,25 +1119,27 @@ public function testPatch() {
     $response = $this->request('PATCH', $url, $request_options);
     $this->assertResourceResponse(200, FALSE, $response);
     $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
-    // Assert that the entity was indeed updated, and that the response body
-    // contains the serialized updated entity.
-    $updated_entity = $this->entityStorage->loadUnchanged($this->entity->id());
-    $updated_entity_normalization = $this->serializer->normalize($updated_entity, static::$format, ['account' => $this->account]);
-    $this->assertSame($updated_entity_normalization, $this->serializer->decode((string) $response->getBody(), static::$format));
-    // Assert that the entity was indeed created using the PATCHed values.
-    foreach ($this->getNormalizedPatchEntity() as $field_name => $field_normalization) {
-      // Some top-level keys in the normalization may not be fields on the
-      // entity (for example '_links' and '_embedded' in the HAL normalization).
-      if ($updated_entity->hasField($field_name)) {
-        // Subset, not same, because we can e.g. send just the target_id for the
-        // bundle in a PATCH request; the response will include more properties.
-        $this->assertArraySubset(static::castToString($field_normalization), $updated_entity->get($field_name)->getValue(), TRUE);
+    if ($this->entity instanceof FieldableEntityInterface) {
+      // Assert that the entity was indeed updated, and that the response body
+      // contains the serialized updated entity.
+      $updated_entity = $this->entityStorage->loadUnchanged($this->entity->id());
+      $updated_entity_normalization = $this->serializer->normalize($updated_entity, static::$format, ['account' => $this->account]);
+      $this->assertSame($updated_entity_normalization, $this->serializer->decode((string) $response->getBody(), static::$format));
+      // Assert that the entity was indeed created using the PATCHed values.
+      foreach ($this->getNormalizedPatchEntity() as $field_name => $field_normalization) {
+        // Some top-level keys in the normalization may not be fields on the
+        // entity (for example '_links' and '_embedded' in the HAL normalization).
+        if ($updated_entity->hasField($field_name)) {
+          // Subset, not same, because we can e.g. send just the target_id for the
+          // bundle in a PATCH request; the response will include more properties.
+          $this->assertArraySubset(static::castToString($field_normalization), $updated_entity->get($field_name)->getValue(), TRUE);
+        }
       }
+      // 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.', $updated_entity->get('field_rest_test')->value);
     }
-    // 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.', $updated_entity->get('field_rest_test')->value);
 
 
     $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
@@ -1141,9 +1165,8 @@ public function testPatch() {
    * Tests a DELETE request for an entity, plus edge cases to ensure good DX.
    */
   public function testDelete() {
-    // @todo Remove this in https://www.drupal.org/node/2300677.
-    if ($this->entity instanceof ConfigEntityInterface) {
-      $this->assertTrue(TRUE, 'DELETEing config entities is not yet supported.');
+    if (!$this->entity->getEntityType()->get('supports_validation')) {
+      $this->assertTrue(TRUE, "This entity type doesn't support DELETEing.");
       return;
     }
 
@@ -1368,7 +1391,15 @@ protected static function getModifiedEntityForPatchTesting(EntityInterface $enti
   protected function makeNormalizationInvalid(array $normalization) {
     // Add a second label to this entity to make it invalid.
     $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
-    $normalization[$label_field][1]['value'] = 'Second Title';
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $normalization[$label_field][1]['value'] = 'Second Title';
+    }
+    else {
+      $normalization[$label_field] = [
+        $normalization[$label_field],
+        'Second title',
+      ];
+    }
 
     return $normalization;
   }
