 .../Comment/CommentHalJsonAnonTest.php             |   2 +-
 .../Comment/CommentHalJsonTestBase.php             |  19 ----
 .../EntityResource/HalEntityNormalizationTrait.php |  18 ----
 .../EntityResource/Node/NodeHalJsonAnonTest.php    |  13 ---
 .../Plugin/Field/FieldType/PathFieldItemList.php   |   9 ++
 .../path/src/Plugin/Field/FieldType/PathItem.php   |  17 +++-
 .../src/Plugin/rest/resource/EntityResource.php    |  76 +++-----------
 .../rest/tests/modules/rest_test/rest_test.module  |   1 +
 .../EntityResource/EntityResourceTestBase.php      | 109 +++++++++++++++------
 .../EntityResource/Node/NodeResourceTestBase.php   |  55 +++++++++++
 .../EntityResource/Term/TermResourceTestBase.php   |  43 ++++++++
 11 files changed, 220 insertions(+), 142 deletions(-)

diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php
index 3edd9b1..9b0cee2 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonAnonTest.php
@@ -25,11 +25,11 @@ class CommentHalJsonAnonTest extends CommentHalJsonTestBase {
    * @see ::setUpAuthorization
    */
   protected static $patchProtectedFieldNames = [
+    'entity_id',
     'changed',
     'thread',
     'entity_type',
     'field_name',
-    'entity_id',
   ];
 
 }
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php
index fc9248a..98539a5 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Comment/CommentHalJsonTestBase.php
@@ -26,25 +26,6 @@
    */
   protected static $mimeType = 'application/hal+json';
 
-  /**
-   * {@inheritdoc}
-   *
-   * The HAL+JSON format causes different PATCH-protected fields. For some
-   * reason, the 'pid' and 'homepage' fields are NOT PATCH-protected, even
-   * though they are for non-HAL+JSON serializations.
-   *
-   * @todo fix in https://www.drupal.org/node/2824271
-   */
-  protected static $patchProtectedFieldNames = [
-    'status',
-    'created',
-    'changed',
-    'thread',
-    'entity_type',
-    'field_name',
-    'entity_id',
-    'uid',
-  ];
 
   /**
    * {@inheritdoc}
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php b/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php
index b64de67..eb01944 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/HalEntityNormalizationTrait.php
@@ -73,24 +73,6 @@ protected function applyHalFieldNormalization(array $normalization) {
   /**
    * {@inheritdoc}
    */
-  protected function removeFieldsFromNormalization(array $normalization, $field_names) {
-    $normalization = parent::removeFieldsFromNormalization($normalization, $field_names);
-    foreach ($field_names as $field_name) {
-      $relation_url = Url::fromUri('base:rest/relation/' . static::$entityTypeId . '/' . $this->entity->bundle() . '/' . $field_name)
-        ->setAbsolute(TRUE)
-        ->toString();
-      $normalization['_links'] = array_diff_key($normalization['_links'], [$relation_url => TRUE]);
-      if (isset($normalization['_embedded'])) {
-        $normalization['_embedded'] = array_diff_key($normalization['_embedded'], [$relation_url => TRUE]);
-      }
-    }
-
-    return array_diff_key($normalization, array_flip($field_names));
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {
     // \Drupal\hal\Normalizer\EntityNormalizer::denormalize(): entity
     // types with bundles MUST send their bundle field to be denormalizable.
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
index 7bbe218..ae4c964 100644
--- a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonAnonTest.php
@@ -33,19 +33,6 @@ class NodeHalJsonAnonTest extends NodeResourceTestBase {
   /**
    * {@inheritdoc}
    */
-  protected static $patchProtectedFieldNames = [
-    'revision_timestamp',
-    'created',
-    'changed',
-    'promote',
-    'sticky',
-    'path',
-    'revision_uid',
-  ];
-
-  /**
-   * {@inheritdoc}
-   */
   protected function getExpectedNormalizedEntity() {
     $default_normalization = parent::getExpectedNormalizedEntity();
 
diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
index a98e718..430adec 100644
--- a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
+++ b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Field\FieldItemList;
+use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Session\AccountInterface;
 
 /**
@@ -72,4 +73,12 @@ protected function ensureLoaded() {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function equals(FieldItemListInterface $list_to_compare) {
+    $this->ensureLoaded();
+    return parent::equals($list_to_compare);
+  }
+
 }
diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
index 4106381..31901ad 100644
--- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
+++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
@@ -114,6 +114,17 @@ public function set($property_name, $value, $notify = TRUE) {
   /**
    * {@inheritdoc}
    */
+  public function setValue($values, $notify = TRUE) {
+    // Also ensure that existing values are loaded when setting a value, this
+    // ensures that it is possible to set a new value immediately after loading
+    // an entity.
+    $this->ensureLoaded();
+    return parent::setValue($values, $notify);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function postSave($update) {
     if (!$update) {
       if ($this->alias) {
@@ -162,7 +173,9 @@ public static function mainPropertyName() {
    *   https://www.drupal.org/node/2392845.
    */
   protected function ensureLoaded() {
-    if (!$this->isLoaded) {
+    static $is_loading = FALSE;
+
+    if (!$this->isLoaded && !$is_loading) {
       $entity = $this->getEntity();
       if (!$entity->isNew()) {
         // @todo Support loading languge neutral aliases in
@@ -172,7 +185,9 @@ protected function ensureLoaded() {
           'langcode' => $this->getLangcode(),
         ]);
         if ($alias) {
+          $is_loading = TRUE;
           $this->setValue($alias);
+          $is_loading = FALSE;
         }
         else {
           // If there is no existing alias, default the langcode to the current
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 5d9849d..1087468 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -11,8 +11,6 @@
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityStorageException;
-use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\TypedData\PrimitiveInterface;
 use Drupal\rest\Plugin\ResourceBase;
 use Drupal\rest\ResourceResponse;
 use Psr\Log\LoggerInterface;
@@ -202,41 +200,6 @@ public function post(EntityInterface $entity = NULL) {
   }
 
   /**
-   * Gets the values from the field item list casted to the correct type.
-   *
-   * Values are casted to the correct type so we can determine whether or not
-   * something has changed. REST formats such as JSON support typed data but
-   * Drupal's database API will return values as strings. Currently, only
-   * primitive data types know how to cast their values to the correct type.
-   *
-   * @param \Drupal\Core\Field\FieldItemListInterface $field_item_list
-   *   The field item list to retrieve its data from.
-   *
-   * @return mixed[][]
-   *   The values from the field item list casted to the correct type. The array
-   *   of values returned is a multidimensional array keyed by delta and the
-   *   property name.
-   */
-  protected function getCastedValueFromFieldItemList(FieldItemListInterface $field_item_list) {
-    $value = $field_item_list->getValue();
-
-    foreach ($value as $delta => $field_item_value) {
-      /** @var \Drupal\Core\Field\FieldItemInterface $field_item */
-      $field_item = $field_item_list->get($delta);
-      $properties = $field_item->getProperties(TRUE);
-      // Foreach field value we check whether we know the underlying property.
-      // If we exists we try to cast the value.
-      foreach ($field_item_value as $property_name => $property_value) {
-        if (isset($properties[$property_name]) && ($property = $field_item->get($property_name)) && $property instanceof PrimitiveInterface) {
-          $value[$delta][$property_name] = $property->getCastedValue();
-        }
-      }
-    }
-
-    return $value;
-  }
-
-  /**
    * Responds to entity PATCH requests.
    *
    * @param \Drupal\Core\Entity\EntityInterface $original_entity
@@ -262,35 +225,26 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
       throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'update'));
     }
 
-    // Overwrite the received properties.
-    $entity_keys = $entity->getEntityType()->getKeys();
+    // Overwrite the received fields.
     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')) {
+      $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'.");
         }
-
-        // 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')) {
+      // 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());
diff --git a/core/modules/rest/tests/modules/rest_test/rest_test.module b/core/modules/rest/tests/modules/rest_test/rest_test.module
index 7df2863..469b4c7 100644
--- a/core/modules/rest/tests/modules/rest_test/rest_test.module
+++ b/core/modules/rest/tests/modules/rest_test/rest_test.module
@@ -15,6 +15,7 @@
  *
  * @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::setUp()
  * @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPost()
+ * @see \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPatch()
  */
 function rest_test_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
   if ($field_definition->getName() === 'field_rest_test') {
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index f39b7d8..72589bd 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -3,13 +3,18 @@
 namespace Drupal\Tests\rest\Functional\EntityResource;
 
 use Drupal\Component\Utility\NestedArray;
+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;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
 use Drupal\Core\Url;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\path\Plugin\Field\FieldType\PathItem;
 use Drupal\rest\ResourceResponseInterface;
 use Drupal\Tests\rest\Functional\ResourceTestBase;
 use GuzzleHttp\RequestOptions;
@@ -925,6 +930,7 @@ public function testPatch() {
     $parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPatchEntity(), static::$format);
     $parseable_invalid_request_body   = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPatchEntity()), static::$format);
     $parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPatchEntity() + ['field_rest_test' => [['value' => $this->randomString()]]], static::$format);
+    $parseable_invalid_request_body_3 = $this->serializer->encode($this->getNormalizedPatchEntity() + ['field_rest_test' => [['value' => 'All the faith he had had had had no effect on the outcome of his life.', 'format' => NULL]]], static::$format);
 
     // The URL and Guzzle request options that will be used in this test. The
     // request options will be modified/expanded throughout this test:
@@ -1036,22 +1042,33 @@ public function testPatch() {
     $this->assertResourceErrorResponse(403, "Access denied on updating field 'field_rest_test'.", $response);
 
 
-    // DX: 403 when sending PATCH request with read-only fields.
-    // First send all fields (the "maximum normalization"). Assert the expected
-    // error message for the first PATCH-protected field. Remove that field from
-    // the normalization, send another request, assert the next PATCH-protected
-    // field error message. And so on.
-    $max_normalization = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format);
+    $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
+
+
+    // DX: 403 when entity contains field without 'edit' nor 'view' access, even
+    // when the value for that field matches the current value. This is allowed
+    // in principle, but leads to information disclosure.
+    $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 updated read-only fields.
+    list($modified_entity, $original_values) = static::getModifiedEntityForPatchTesting($this->entity);
+    // Send PATCH request by serializing the modified entity, assert the error
+    // response, change the modified entity field that caused the error response
+    // back to its original value, repeat.
     for ($i = 0; $i < count(static::$patchProtectedFieldNames); $i++) {
-      $max_normalization = $this->removeFieldsFromNormalization($max_normalization, array_slice(static::$patchProtectedFieldNames, 0, $i));
-      $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format);
+      $patch_protected_field_name = static::$patchProtectedFieldNames[$i];
+      $request_options[RequestOptions::BODY] = $this->serializer->serialize($modified_entity, static::$format);
       $response = $this->request('PATCH', $url, $request_options);
-      $this->assertResourceErrorResponse(403, "Access denied on updating field '" . static::$patchProtectedFieldNames[$i] . "'.", $response);
+      $this->assertResourceErrorResponse(403, "Access denied on updating field '" . $patch_protected_field_name . "'.", $response);
+      $modified_entity->get($patch_protected_field_name)->setValue($original_values[$patch_protected_field_name]);
     }
 
-    // 200 for well-formed request that sends the maximum number of fields.
-    $max_normalization = $this->removeFieldsFromNormalization($max_normalization, static::$patchProtectedFieldNames);
-    $request_options[RequestOptions::BODY] = $this->serializer->serialize($max_normalization, static::$format);
+    // 200 for well-formed PATCH request that sends all fields (even including
+    // read-only ones, but with unchanged values).
+    $valid_request_body = $this->getNormalizedPatchEntity() + $this->serializer->normalize($this->entity, static::$format);
+    $request_options[RequestOptions::BODY] = $this->serializer->serialize($valid_request_body, static::$format);
     $response = $this->request('PATCH', $url, $request_options);
     $this->assertResourceResponse(200, FALSE, $response);
 
@@ -1289,37 +1306,71 @@ protected function getEntityResourcePostUrl() {
   }
 
   /**
-   * Makes the given entity normalization invalid.
+   * Clones the given entity and modifies all PATCH-protected fields.
    *
-   * @param array $normalization
-   *   An entity normalization.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being tested and to modify.
    *
    * @return array
-   *   The updated entity normalization, now invalid.
+   *   Contains two items:
+   *   1. The modified entity object.
+   *   2. The original field values, keyed by field name.
+   *
+   * @internal
    */
-  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';
+  protected static function getModifiedEntityForPatchTesting(EntityInterface $entity) {
+    $modified_entity = clone $entity;
+    $original_values = [];
+    foreach (static::$patchProtectedFieldNames as $field_name) {
+      $field = $modified_entity->get($field_name);
+      $original_values[$field_name] = $field->getValue();
+      switch ($field->getItemDefinition()->getClass()) {
+        case EntityReferenceItem::class:
+          // EntityReferenceItem::generateSampleValue() picks one of the last 50
+          // entities of the supported type & bundle. We don't care if the value
+          // is valid, we only care that it's different.
+          $field->setValue(['target_id' => 99999]);
+          break;
+        case BooleanItem::class:
+          // BooleanItem::generateSampleValue() picks either 0 or 1. So a 50%
+          // chance of not picking a different value.
+          $field->value = ((int) $field->value) === 1 ? '0' : '1';
+          break;
+        case PathItem::class:
+          // PathItem::generateSampleValue() doesn't set a PID, which causes
+          // PathItem::postSave() to fail. Keep the PID (and other properties),
+          // just modify the alias.
+          $value = $field->getValue();
+          $value['alias'] = str_replace(' ', '-', strtolower((new Random())->sentences(3)));
+          $field->setValue($value);
+          break;
+        default:
+          $original_field = clone $field;
+          while ($field->equals($original_field)) {
+            $field->generateSampleItems();
+          }
+          break;
+      }
+    }
 
-    return $normalization;
+    return [$modified_entity, $original_values];
   }
 
   /**
-   * Removes fields from a normalization.
+   * Makes the given entity normalization invalid.
    *
    * @param array $normalization
    *   An entity normalization.
-   * @param string[] $field_names
-   *   The field names to remove from the entity normalization.
    *
    * @return array
-   *   The updated entity normalization.
-   *
-   * @see ::testPatch
+   *   The updated entity normalization, now invalid.
    */
-  protected function removeFieldsFromNormalization(array $normalization, $field_names) {
-    return array_diff_key($normalization, array_flip($field_names));
+  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';
+
+    return $normalization;
   }
 
   /**
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
index b4fc553..bbdd0e5 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
@@ -7,6 +7,7 @@
 use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
 use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
 use Drupal\user\Entity\User;
+use GuzzleHttp\RequestOptions;
 
 abstract class NodeResourceTestBase extends EntityResourceTestBase {
 
@@ -215,4 +216,58 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
     return parent::getExpectedUnauthorizedAccessMessage($method);
   }
 
+  /**
+   * Tests PATCHing a node's path with and without 'create url aliases'.
+   *
+   * For a positive test, see the similar test coverage for Term.
+   *
+   * @see \Drupal\Tests\rest\Functional\EntityResource\Term\TermResourceTestBase::testPatchPath()
+   */
+  public function testPatchPath() {
+    $this->initAuthentication();
+    $this->provisionEntityResource();
+    $this->setUpAuthorization('GET');
+    $this->setUpAuthorization('PATCH');
+
+    $url = $this->getEntityResourceUrl()->setOption('query', ['_format' => static::$format]);
+
+    // GET node's current normalization.
+    $response = $this->request('GET', $url, $this->getAuthenticationRequestOptions('GET'));
+    $normalization = $this->serializer->decode((string) $response->getBody(), static::$format);
+
+    // @todo In https://www.drupal.org/node/2824851, we will be able to stop
+    //       unsetting these fields from the normalization, because
+    //       EntityResource::patch() will ignore any fields that are sent that
+    //       match the current value (and obviously we're sending the current
+    //       value).
+    unset($normalization['revision_timestamp']);
+    unset($normalization['revision_uid']);
+    unset($normalization['created']);
+    unset($normalization['changed']);
+    unset($normalization['promote']);
+    unset($normalization['sticky']);
+
+    // Change node's path alias.
+    $normalization['path'][0]['alias'] .= 's-rule-the-world';
+
+    // Create node PATCH request.
+    $request_options = [];
+    $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
+    $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
+    $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
+
+    // PATCH request: 403 when creating URL aliases unauthorized.
+    $response = $this->request('PATCH', $url, $request_options);
+    $this->assertResourceErrorResponse(403, "Access denied on updating field 'path'.", $response);
+
+    // Grant permission to create URL aliases.
+    $this->grantPermissionsToTestedRole(['create url aliases']);
+
+    // Repeat PATCH request: 200.
+    $response = $this->request('PATCH', $url, $request_options);
+    $this->assertResourceResponse(200, FALSE, $response);
+    $updated_normalization = $this->serializer->decode((string) $response->getBody(), static::$format);
+    $this->assertSame($normalization['path'], $updated_normalization['path']);
+  }
+
 }
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
index 44a4e83..68e6a2e 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
@@ -6,6 +6,7 @@
 use Drupal\taxonomy\Entity\Vocabulary;
 use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
 use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
+use GuzzleHttp\RequestOptions;
 
 abstract class TermResourceTestBase extends EntityResourceTestBase {
 
@@ -178,4 +179,46 @@ protected function getExpectedUnauthorizedAccessMessage($method) {
     }
   }
 
+  /**
+   * Tests PATCHing a term's path.
+   *
+   * For a negative test, see the similar test coverage for Node.
+   *
+   * @see \Drupal\Tests\rest\Functional\EntityResource\Node\NodeResourceTestBase::testPatchPath()
+   */
+  public function testPatchPath() {
+    $this->initAuthentication();
+    $this->provisionEntityResource();
+    $this->setUpAuthorization('GET');
+    $this->setUpAuthorization('PATCH');
+
+    $url = $this->getEntityResourceUrl()->setOption('query', ['_format' => static::$format]);
+
+    // GET term's current normalization.
+    $response = $this->request('GET', $url, $this->getAuthenticationRequestOptions('GET'));
+    $normalization = $this->serializer->decode((string) $response->getBody(), static::$format);
+
+    // @todo In https://www.drupal.org/node/2824851, we will be able to stop
+    //       unsetting these fields from the normalization, because
+    //       EntityResource::patch() will ignore any fields that are sent that
+    //       match the current value (and obviously we're sending the current
+    //       value).
+    unset($normalization['changed']);
+
+    // Change term's path alias.
+    $normalization['path'][0]['alias'] .= 's-rule-the-world';
+
+    // Create term PATCH request.
+    $request_options = [];
+    $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
+    $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
+    $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
+
+    // PATCH request: 200.
+    $response = $this->request('PATCH', $url, $request_options);
+    $this->assertResourceResponse(200, FALSE, $response);
+    $updated_normalization = $this->serializer->decode((string) $response->getBody(), static::$format);
+    $this->assertSame($normalization['path'], $updated_normalization['path']);
+  }
+
 }
