 src/Normalizer/CacheableDependencyTrait.php        | 70 ++++++++++++++++++++++
 src/Normalizer/ConfigEntityNormalizer.php          |  9 +--
 src/Normalizer/EntityNormalizer.php                | 53 ++--------------
 src/Normalizer/EntityReferenceFieldNormalizer.php  | 10 +++-
 src/Normalizer/FieldItemNormalizer.php             | 15 ++++-
 src/Normalizer/FieldNormalizer.php                 | 43 +++++++++++--
 src/Normalizer/HttpExceptionNormalizer.php         | 10 +++-
 .../JsonApiDocumentTopLevelNormalizer.php          |  6 +-
 src/Normalizer/Relationship.php                    | 14 ++++-
 src/Normalizer/RelationshipItemNormalizer.php      | 25 ++++----
 src/Normalizer/RelationshipNormalizer.php          |  8 ++-
 .../Value/CacheableDependenciesMergerTrait.php     | 20 +++++++
 src/Normalizer/Value/EntityNormalizerValue.php     | 14 ++---
 src/Normalizer/Value/FieldItemNormalizerValue.php  | 51 +++++-----------
 src/Normalizer/Value/FieldNormalizerValue.php      | 34 +++++------
 .../Value/FieldNormalizerValueInterface.php        | 24 +-------
 src/Normalizer/Value/NullFieldNormalizerValue.php  | 34 ++++++-----
 .../Value/RelationshipItemNormalizerValue.php      | 50 ++++++++++++----
 .../Value/RelationshipNormalizerValue.php          |  8 ++-
 tests/src/Functional/BlockContentTest.php          | 11 +---
 tests/src/Functional/CommentTest.php               | 19 +++---
 tests/src/Functional/TermTest.php                  | 20 ++++---
 .../JsonApiDocumentTopLevelNormalizerTest.php      | 69 +++++++++++++++++++++
 .../Normalizer/Value/EntityNormalizerValueTest.php | 36 +++++++++++
 .../Value/FieldItemNormalizerValueTest.php         |  9 +--
 .../Normalizer/Value/FieldNormalizerValueTest.php  | 43 +++++++++++--
 .../Value/RelationshipItemNormalizerValueTest.php  |  3 +-
 .../Value/RelationshipNormalizerValueTest.php      | 47 ++++++++++++++-
 28 files changed, 521 insertions(+), 234 deletions(-)

diff --git a/src/Normalizer/CacheableDependencyTrait.php b/src/Normalizer/CacheableDependencyTrait.php
new file mode 100644
index 0000000..5253a05
--- /dev/null
+++ b/src/Normalizer/CacheableDependencyTrait.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
+
+/**
+ * @internal
+ * @deprecated Remove when JSON API requires Drupal 8.5 or newer, update all users to \Drupal\Core\Cache\CacheableDependencyTrait instead.
+ */
+trait CacheableDependencyTrait {
+
+  /**
+   * Cache contexts.
+   *
+   * @var string[]
+   */
+  protected $cacheContexts = [];
+
+  /**
+   * Cache tags.
+   *
+   * @var string[]
+   */
+  protected $cacheTags = [];
+
+  /**
+   * Cache max-age.
+   *
+   * @var int
+   */
+  protected $cacheMaxAge = Cache::PERMANENT;
+
+  /**
+   * Sets cacheability; useful for value object constructors.
+   *
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $cacheability
+   *   The cacheability to set.
+   *
+   * @return $this
+   */
+  protected function setCacheability(CacheableDependencyInterface $cacheability) {
+    $this->cacheContexts = $cacheability->getCacheContexts();
+    $this->cacheTags = $cacheability->getCacheTags();
+    $this->cacheMaxAge = $cacheability->getCacheMaxAge();
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return $this->cacheTags;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return $this->cacheContexts;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return $this->cacheMaxAge;
+  }
+
+}
diff --git a/src/Normalizer/ConfigEntityNormalizer.php b/src/Normalizer/ConfigEntityNormalizer.php
index dd4f178..2bc8b79 100644
--- a/src/Normalizer/ConfigEntityNormalizer.php
+++ b/src/Normalizer/ConfigEntityNormalizer.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\jsonapi\Normalizer;
 
+use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
 use Drupal\jsonapi\Normalizer\Value\ConfigFieldItemNormalizerValue;
 use Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue;
@@ -46,12 +47,12 @@ class ConfigEntityNormalizer extends EntityNormalizer {
    * {@inheritdoc}
    */
   protected function serializeField($field, array $context, $format) {
-    $output = new FieldNormalizerValue(
+    return new FieldNormalizerValue(
+      AccessResult::allowed(),
       [new ConfigFieldItemNormalizerValue($field)],
-      1
+      1,
+      'attributes'
     );
-    $output->setPropertyType('attributes');
-    return $output;
   }
 
   /**
diff --git a/src/Normalizer/EntityNormalizer.php b/src/Normalizer/EntityNormalizer.php
index bb07c12..ba99c60 100644
--- a/src/Normalizer/EntityNormalizer.php
+++ b/src/Normalizer/EntityNormalizer.php
@@ -99,34 +99,13 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
       if (!in_array($field_name, $field_names)) {
         continue;
       }
-      $normalizer_values[$field_name] = $this->serializeField($field, $context, $format);
+      $normalized_field = $this->serializeField($field, $context, $format);
+      assert($normalized_field instanceof FieldNormalizerValueInterface);
+      $normalizer_values[$field_name] = $normalized_field;
     }
 
     $link_context = ['link_manager' => $this->linkManager];
-    $output = new EntityNormalizerValue($normalizer_values, $context, $entity, $link_context);
-    // Add the entity level cacheability metadata.
-    $output->addCacheableDependency($entity);
-    $output->addCacheableDependency($output);
-    // Add the field level cacheability metadata.
-    array_walk($normalizer_values, function ($normalizer_value) {
-      if ($normalizer_value instanceof RefinableCacheableDependencyInterface) {
-        $normalizer_value->addCacheableDependency($normalizer_value);
-      }
-    });
-    return $output;
-  }
-
-  /**
-   * Checks if the passed field is a relationship field.
-   *
-   * @param mixed $field
-   *   The field.
-   *
-   * @return bool
-   *   TRUE if it's a JSON API relationship.
-   */
-  protected function isRelationship($field) {
-    return $field instanceof EntityReferenceFieldItemList || $field instanceof Relationship;
+    return new EntityNormalizerValue($normalizer_values, $context, $entity, $link_context);
   }
 
   /**
@@ -213,29 +192,7 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
    *   The normalized value.
    */
   protected function serializeField($field, array $context, $format) {
-    /* @var \Drupal\Core\Field\FieldItemListInterface|\Drupal\jsonapi\Normalizer\Relationship $field */
-    // Continue if the current user does not have access to view this field.
-    $access = $field->access('view', $context['account'], TRUE);
-    $context['cacheable_metadata']->addCacheableDependency($access);
-    if ($field instanceof AccessibleInterface && !$access->isAllowed()) {
-      return (new NullFieldNormalizerValue())->addCacheableDependency($access);
-    }
-    /** @var \Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue $output */
-    $output = $this->serializer->normalize($field, $format, $context);
-    if (!$output instanceof FieldNormalizerValueInterface) {
-      return new NullFieldNormalizerValue();
-    }
-    $is_relationship = $this->isRelationship($field);
-    $property_type = $is_relationship ? 'relationships' : 'attributes';
-    $output->setPropertyType($property_type);
-
-    if ($output instanceof RefinableCacheableDependencyInterface) {
-      // Add the cache dependency to the field level object because we want to
-      // allow the field normalizers to add extra cacheability metadata.
-      $output->addCacheableDependency($access);
-    }
-
-    return $output;
+    return $this->serializer->normalize($field, $format, $context);
   }
 
   /**
diff --git a/src/Normalizer/EntityReferenceFieldNormalizer.php b/src/Normalizer/EntityReferenceFieldNormalizer.php
index 7a4d6d6..e1dc063 100644
--- a/src/Normalizer/EntityReferenceFieldNormalizer.php
+++ b/src/Normalizer/EntityReferenceFieldNormalizer.php
@@ -9,6 +9,7 @@ use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
 use Drupal\Core\Field\FieldTypePluginManagerInterface;
 use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
 use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
+use Drupal\jsonapi\Normalizer\Value\NullFieldNormalizerValue;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
 use Drupal\jsonapi\Resource\EntityCollection;
 use Drupal\jsonapi\LinkManager\LinkManager;
@@ -81,6 +82,13 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
    * {@inheritdoc}
    */
   public function normalize($field, $format = NULL, array $context = []) {
+    /* @var \Drupal\Core\Field\FieldItemListInterface $field */
+
+    $field_access = $field->access('view', $context['account'], TRUE);
+    if (!$field_access->isAllowed()) {
+      return new NullFieldNormalizerValue($field_access, 'relationships');
+    }
+
     /* @var $field \Drupal\Core\Field\FieldItemListInterface */
     // Build the relationship object based on the Entity Reference and normalize
     // that object instead.
@@ -121,7 +129,7 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
       $entity_list[] = $this->entityRepository->getTranslationFromContext($entity);
     }
     $entity_collection = new EntityCollection($entity_list);
-    $relationship = new Relationship($this->resourceTypeRepository, $field->getName(), $entity_collection, $field->getEntity(), $cardinality, $main_property, $entity_list_metadata);
+    $relationship = new Relationship($this->resourceTypeRepository, $field->getName(), $entity_collection, $field->getEntity(), $field_access, $cardinality, $main_property, $entity_list_metadata);
     return $this->serializer->normalize($relationship, $format, $context);
   }
 
diff --git a/src/Normalizer/FieldItemNormalizer.php b/src/Normalizer/FieldItemNormalizer.php
index abc59da..298d43a 100644
--- a/src/Normalizer/FieldItemNormalizer.php
+++ b/src/Normalizer/FieldItemNormalizer.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\jsonapi\Normalizer;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Field\FieldItemInterface;
 use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
 use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
@@ -30,6 +31,11 @@ class FieldItemNormalizer extends NormalizerBase {
 
   /**
    * {@inheritdoc}
+   *
+   * This normalizer exists JSON API normalizer land and enters the land of
+   * Drupal core's serialization system. That system was never designed with
+   * cacheability in mind, and hence bubbles cacheability out of band. This must
+   * catch it, and pass it to the value object that JSON API uses.
    */
   public function normalize($field_item, $format = NULL, array $context = []) {
     /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
@@ -40,6 +46,10 @@ class FieldItemNormalizer extends NormalizerBase {
     if (floatval(\Drupal::VERSION) >= 8.5) {
       $field_item = TypedDataInternalPropertiesHelper::getNonInternalProperties($field_item);
     }
+
+    // @todo Use the constant \Drupal\serialization\Normalizer\CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY instead of the 'cacheability' string when JSON API requires Drupal 8.5 or newer.
+    $context['cacheability'] = new CacheableMetadata();
+
     foreach ($field_item as $property_name => $property) {
       $values[$property_name] = $this->serializer->normalize($property, $format, $context);
     }
@@ -47,7 +57,10 @@ class FieldItemNormalizer extends NormalizerBase {
     if (isset($context['langcode'])) {
       $values['lang'] = $context['langcode'];
     }
-    return new FieldItemNormalizerValue($values);
+    // @todo Use the constant \Drupal\serialization\Normalizer\CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY instead of the 'cacheability' string when JSON API requires Drupal 8.5 or newer.
+    $value = new FieldItemNormalizerValue($values, $context['cacheability']);
+    unset($context['cacheability']);
+    return $value;
   }
 
   /**
diff --git a/src/Normalizer/FieldNormalizer.php b/src/Normalizer/FieldNormalizer.php
index 1d8788e..fd354d8 100644
--- a/src/Normalizer/FieldNormalizer.php
+++ b/src/Normalizer/FieldNormalizer.php
@@ -2,8 +2,12 @@
 
 namespace Drupal\jsonapi\Normalizer;
 
+use Drupal\Component\Assertion\Inspector;
+use Drupal\Core\Field\EntityReferenceFieldItemList;
 use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
 use Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\NullFieldNormalizerValue;
 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 
 /**
@@ -32,7 +36,37 @@ class FieldNormalizer extends NormalizerBase {
    */
   public function normalize($field, $format = NULL, array $context = []) {
     /* @var \Drupal\Core\Field\FieldItemListInterface $field */
-    return $this->normalizeFieldItems($field, $format, $context);
+
+    $access = $field->access('view', $context['account'], TRUE);
+    $property_type = static::isRelationship($field) ? 'relationships' : 'attributes';
+
+    if ($access->isAllowed()) {
+      $normalized_field_items = $this->normalizeFieldItems($field, $format, $context);
+      assert(Inspector::assertAll(function ($v) {
+        return $v instanceof FieldItemNormalizerValue;
+      }, $normalized_field_items));
+
+      $cardinality = $field->getFieldDefinition()
+        ->getFieldStorageDefinition()
+        ->getCardinality();
+      return new FieldNormalizerValue($access, $normalized_field_items, $cardinality, $property_type);
+    }
+    else {
+      return new NullFieldNormalizerValue($access, $property_type);
+    }
+  }
+
+  /**
+   * Checks if the passed field is a relationship field.
+   *
+   * @param mixed $field
+   *   The field.
+   *
+   * @return bool
+   *   TRUE if it's a JSON API relationship.
+   */
+  protected static function isRelationship($field) {
+    return $field instanceof EntityReferenceFieldItemList || $field instanceof Relationship;
   }
 
   /**
@@ -52,7 +86,7 @@ class FieldNormalizer extends NormalizerBase {
    * @param array $context
    *   The context array.
    *
-   * @return \Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue
+   * @return \Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue[]
    *   The array of normalized field items.
    */
   protected function normalizeFieldItems(FieldItemListInterface $field, $format, array $context) {
@@ -62,10 +96,7 @@ class FieldNormalizer extends NormalizerBase {
         $normalizer_items[] = $this->serializer->normalize($field_item, $format, $context);
       }
     }
-    $cardinality = $field->getFieldDefinition()
-      ->getFieldStorageDefinition()
-      ->getCardinality();
-    return new FieldNormalizerValue($normalizer_items, $cardinality);
+    return $normalizer_items;
   }
 
 }
diff --git a/src/Normalizer/HttpExceptionNormalizer.php b/src/Normalizer/HttpExceptionNormalizer.php
index be5a957..9a61d44 100644
--- a/src/Normalizer/HttpExceptionNormalizer.php
+++ b/src/Normalizer/HttpExceptionNormalizer.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\jsonapi\Normalizer;
 
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
@@ -49,12 +51,16 @@ class HttpExceptionNormalizer extends NormalizerBase {
     $errors = $this->buildErrorObjects($object);
 
     $errors = array_map(function ($error) {
-      return new FieldItemNormalizerValue([$error]);
+      // @todo Either this should not use FieldItemNormalizerValue, or FieldItemNormalizerValue needs to be renamed to not be semantically coupled to "fields".
+      return new FieldItemNormalizerValue([$error], new CacheableMetadata());
     }, $errors);
 
+    // @todo The access result, cardinality and property type make no sense for HTTP exceptions, but it's because HttpExceptionNormalizerValue inappropriately subclasses FieldNormalizerValue
     return new HttpExceptionNormalizerValue(
+      AccessResult::allowed(),
       $errors,
-      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+      FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+      'attributes'
     );
   }
 
diff --git a/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
index 1f33db5..4266a48 100644
--- a/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
+++ b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
@@ -230,11 +230,7 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm
     }
 
     if ($data instanceof EntityReferenceFieldItemListInterface) {
-      $output = $this->serializer->normalize($data, $format, $context);
-      // The only normalizer value that computes nested includes automatically
-      // is the JsonApiDocumentTopLevelNormalizerValue.
-      $output->setIncludes($output->getAllIncludes());
-      return $output;
+      return $this->serializer->normalize($data, $format, $context);
     }
     else {
       $is_collection = $data instanceof EntityCollection;
diff --git a/src/Normalizer/Relationship.php b/src/Normalizer/Relationship.php
index 0ce1284..57b3661 100644
--- a/src/Normalizer/Relationship.php
+++ b/src/Normalizer/Relationship.php
@@ -3,6 +3,8 @@
 namespace Drupal\jsonapi\Normalizer;
 
 use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Session\AccountInterface;
@@ -18,7 +20,9 @@ use Drupal\jsonapi\Resource\EntityCollection;
  *
  * @internal
  */
-class Relationship implements AccessibleInterface {
+class Relationship implements AccessibleInterface, CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
 
   /**
    * Cardinality.
@@ -66,6 +70,9 @@ class Relationship implements AccessibleInterface {
    *   A collection of entities.
    * @param \Drupal\Core\Entity\EntityInterface $host_entity
    *   The host entity.
+   * @param \Drupal\Core\Access\AccessResultInterface $view_access
+   *   The 'view' field access result. (This value object is only ever used for
+   *   normalization, and hence only for 'view' access.
    * @param int $cardinality
    *   The relationship cardinality.
    * @param string $target_key
@@ -74,11 +81,14 @@ class Relationship implements AccessibleInterface {
    *   An array of additional properties stored by the field and that will be
    *   added to the meta in the relationship.
    */
-  public function __construct(ResourceTypeRepositoryInterface $resource_type_repository, $field_name, EntityCollection $entities, EntityInterface $host_entity, $cardinality = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, $target_key = 'target_id', array $entity_list_metadata = []) {
+  public function __construct(ResourceTypeRepositoryInterface $resource_type_repository, $field_name, EntityCollection $entities, EntityInterface $host_entity, AccessResultInterface $view_access, $cardinality = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, $target_key = 'target_id', array $entity_list_metadata = []) {
     $this->resourceTypeRepository = $resource_type_repository;
     $this->propertyName = $field_name;
     $this->cardinality = $cardinality;
     $this->hostEntity = $host_entity;
+
+    $this->setCacheability($view_access);
+
     $this->items = [];
     foreach ($entities as $key => $entity) {
       $this->items[] = new RelationshipItem(
diff --git a/src/Normalizer/RelationshipItemNormalizer.php b/src/Normalizer/RelationshipItemNormalizer.php
index 93bc6af..3880ec3 100644
--- a/src/Normalizer/RelationshipItemNormalizer.php
+++ b/src/Normalizer/RelationshipItemNormalizer.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\jsonapi\Normalizer;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\jsonapi\Normalizer\Value\RelationshipItemNormalizerValue;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
@@ -65,10 +66,6 @@ class RelationshipItemNormalizer extends FieldItemNormalizer {
     if (isset($context['langcode'])) {
       $values['lang'] = $context['langcode'];
     }
-    $normalizer_value = new RelationshipItemNormalizerValue(
-      $values,
-      $relationship_item->getTargetResourceType()
-    );
 
     $host_field_name = $relationship_item->getParent()->getPropertyName();
     if (!empty($context['include']) && in_array($host_field_name, $context['include'])) {
@@ -77,17 +74,17 @@ class RelationshipItemNormalizer extends FieldItemNormalizer {
       $included_normalizer_value = $this
         ->jsonapiDocumentToplevelNormalizer
         ->buildNormalizerValue($entity_and_access['entity'], $format, $context);
-      $normalizer_value->setInclude($included_normalizer_value);
-      $normalizer_value->addCacheableDependency($entity_and_access['access']);
-      $normalizer_value->addCacheableDependency($included_normalizer_value);
-      // Add the cacheable dependency of the included item directly to the
-      // response cacheable metadata. This is similar to the flatten include
-      // data structure, instead of a content graph.
-      if (!empty($context['cacheable_metadata'])) {
-        $context['cacheable_metadata']->addCacheableDependency($normalizer_value);
-      }
     }
-    return $normalizer_value;
+    else {
+      $included_normalizer_value = NULL;
+    }
+
+    return new RelationshipItemNormalizerValue(
+      $values,
+      new CacheableMetadata(),
+      $relationship_item->getTargetResourceType(),
+      $included_normalizer_value
+    );
   }
 
   /**
diff --git a/src/Normalizer/RelationshipNormalizer.php b/src/Normalizer/RelationshipNormalizer.php
index a28c1c3..6cd59f6 100644
--- a/src/Normalizer/RelationshipNormalizer.php
+++ b/src/Normalizer/RelationshipNormalizer.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\jsonapi\Normalizer;
 
+use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\jsonapi\Normalizer\Value\RelationshipNormalizerValue;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
@@ -90,7 +91,12 @@ class RelationshipNormalizer extends NormalizerBase {
       'link_manager' => $this->linkManager,
       'resource_type' => $context['resource_type'],
     ];
-    return new RelationshipNormalizerValue($normalizer_items, $cardinality, $link_context);
+    // If this is called, Relationship access is always allowed. The
+    // cacheability of the access result is carried by the Relationship value
+    // object. Therefore, we can safely construct an access result object here.
+    // @see \Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer::normalize()
+    $relationship_access = AccessResult::allowed()->addCacheableDependency($relationship);
+    return new RelationshipNormalizerValue($relationship_access, $normalizer_items, $cardinality, $link_context);
   }
 
   /**
diff --git a/src/Normalizer/Value/CacheableDependenciesMergerTrait.php b/src/Normalizer/Value/CacheableDependenciesMergerTrait.php
new file mode 100644
index 0000000..9b48601
--- /dev/null
+++ b/src/Normalizer/Value/CacheableDependenciesMergerTrait.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer\Value;
+
+use Drupal\Core\Cache\CacheableMetadata;
+
+/**
+ * @internal
+ */
+trait CacheableDependenciesMergerTrait {
+
+  protected static function mergeCacheableDependencies(array $dependencies) {
+    $merged_cacheability = new CacheableMetadata();
+    array_walk($dependencies, function ($dependency) use ($merged_cacheability) {
+      $merged_cacheability->addCacheableDependency($dependency);
+    });
+    return $merged_cacheability;
+  }
+
+}
diff --git a/src/Normalizer/Value/EntityNormalizerValue.php b/src/Normalizer/Value/EntityNormalizerValue.php
index b798f60..7b696d1 100644
--- a/src/Normalizer/Value/EntityNormalizerValue.php
+++ b/src/Normalizer/Value/EntityNormalizerValue.php
@@ -2,18 +2,19 @@
 
 namespace Drupal\jsonapi\Normalizer\Value;
 
-use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
-use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\jsonapi\Normalizer\CacheableDependencyTrait;
 
 /**
  * Helps normalize entities in compliance with the JSON API spec.
  *
  * @internal
  */
-class EntityNormalizerValue implements ValueExtractorInterface, RefinableCacheableDependencyInterface {
+class EntityNormalizerValue implements ValueExtractorInterface, CacheableDependencyInterface {
 
-  use RefinableCacheableDependencyTrait;
+  use CacheableDependencyTrait;
+  use CacheableDependenciesMergerTrait;
 
   /**
    * The values.
@@ -64,6 +65,8 @@ class EntityNormalizerValue implements ValueExtractorInterface, RefinableCacheab
    *   relationship.
    */
   public function __construct(array $values, array $context, EntityInterface $entity, array $link_context) {
+    $this->setCacheability(static::mergeCacheableDependencies(array_merge([$entity], $values)));
+
     $this->values = array_filter($values, function ($value) {
       return !($value instanceof NullFieldNormalizerValue);
     });
@@ -80,9 +83,6 @@ class EntityNormalizerValue implements ValueExtractorInterface, RefinableCacheab
     }, []);
     // Filter the empty values.
     $this->includes = array_filter($this->includes);
-    array_walk($this->includes, function ($include) {
-      $this->addCacheableDependency($include);
-    });
   }
 
   /**
diff --git a/src/Normalizer/Value/FieldItemNormalizerValue.php b/src/Normalizer/Value/FieldItemNormalizerValue.php
index e242eac..bb290e4 100644
--- a/src/Normalizer/Value/FieldItemNormalizerValue.php
+++ b/src/Normalizer/Value/FieldItemNormalizerValue.php
@@ -1,13 +1,17 @@
 <?php
 
 namespace Drupal\jsonapi\Normalizer\Value;
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\jsonapi\Normalizer\CacheableDependencyTrait;
 
 /**
  * Helps normalize field items in compliance with the JSON API spec.
  *
  * @internal
  */
-class FieldItemNormalizerValue implements ValueExtractorInterface {
+class FieldItemNormalizerValue implements CacheableDependencyInterface {
+
+  use CacheableDependencyTrait;
 
   /**
    * Raw values.
@@ -17,20 +21,22 @@ class FieldItemNormalizerValue implements ValueExtractorInterface {
   protected $raw;
 
   /**
-   * Included entity objects.
-   *
-   * @var \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue
-   */
-  protected $include;
-
-  /**
    * Instantiate a FieldItemNormalizerValue object.
    *
    * @param array $values
    *   The normalized result.
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $values_cacheability
+   *   The cacheability of the normalized result. This cacheability is not part
+   *   of $values because field items are normalized by Drupal core's
+   *   serialization system, which was never designed with cacheability in mind.
+   *   FieldItemNormalizer::normalize() must catch the out-of-band bubbled
+   *   cacheability and then passes it to this value object.
+   *
+   * @see \Drupal\jsonapi\Normalizer\FieldItemNormalizer::normalize()
    */
-  public function __construct(array $values) {
+  public function __construct(array $values, CacheableDependencyInterface $values_cacheability) {
     $this->raw = $values;
+    $this->setCacheability($values_cacheability);
   }
 
   /**
@@ -44,33 +50,6 @@ class FieldItemNormalizerValue implements ValueExtractorInterface {
   }
 
   /**
-   * {@inheritdoc}
-   */
-  public function rasterizeIncludes() {
-    return $this->include->rasterizeValue();
-  }
-
-  /**
-   * Add an include.
-   *
-   * @param ValueExtractorInterface $include
-   *   The included entity.
-   */
-  public function setInclude(ValueExtractorInterface $include) {
-    $this->include = $include;
-  }
-
-  /**
-   * Gets the include.
-   *
-   * @return \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue
-   *   The include.
-   */
-  public function getInclude() {
-    return $this->include;
-  }
-
-  /**
    * Rasterizes a value recursively.
    *
    * This is mainly for configuration entities where a field can be a tree of
diff --git a/src/Normalizer/Value/FieldNormalizerValue.php b/src/Normalizer/Value/FieldNormalizerValue.php
index e43e4f2..0c2bc62 100644
--- a/src/Normalizer/Value/FieldNormalizerValue.php
+++ b/src/Normalizer/Value/FieldNormalizerValue.php
@@ -2,7 +2,8 @@
 
 namespace Drupal\jsonapi\Normalizer\Value;
 
-use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\jsonapi\Normalizer\CacheableDependencyTrait;
 
 /**
  * Helps normalize fields in compliance with the JSON API spec.
@@ -11,7 +12,8 @@ use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
  */
 class FieldNormalizerValue implements FieldNormalizerValueInterface {
 
-  use RefinableCacheableDependencyTrait;
+  use CacheableDependencyTrait;
+  use CacheableDependenciesMergerTrait;
 
   /**
    * The values.
@@ -44,21 +46,29 @@ class FieldNormalizerValue implements FieldNormalizerValueInterface {
   /**
    * Instantiate a FieldNormalizerValue object.
    *
+   * @param \Drupal\Core\Access\AccessResultInterface $field_access_result
+   *   The field access result.
    * @param \Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue[] $values
    *   The normalized result.
    * @param int $cardinality
    *   The cardinality of the field list.
+   * @param string $property_type
+   *   The property type of the field: 'attributes' or 'relationships'.
    */
-  public function __construct(array $values, $cardinality) {
+  public function __construct(AccessResultInterface $field_access_result, array $values, $cardinality, $property_type) {
+    assert($property_type === 'attributes' || $property_type === 'relationships');
+    $this->setCacheability(static::mergeCacheableDependencies(array_merge([$field_access_result], $values)));
+
     $this->values = $values;
     $this->includes = array_map(function ($value) {
-      if (!$value instanceof FieldItemNormalizerValue) {
-        return new NullFieldNormalizerValue();
+      if (!$value instanceof RelationshipItemNormalizerValue) {
+        return NULL;
       }
       return $value->getInclude();
     }, $values);
     $this->includes = array_filter($this->includes);
     $this->cardinality = $cardinality;
+    $this->propertyType = $property_type;
   }
 
   /**
@@ -106,20 +116,6 @@ class FieldNormalizerValue implements FieldNormalizerValueInterface {
   /**
    * {@inheritdoc}
    */
-  public function setPropertyType($property_type) {
-    $this->propertyType = $property_type;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setIncludes(array $includes) {
-    $this->includes = $includes;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   public function getAllIncludes() {
     $nested_includes = array_map(function ($include) {
       return $include->getIncludes();
diff --git a/src/Normalizer/Value/FieldNormalizerValueInterface.php b/src/Normalizer/Value/FieldNormalizerValueInterface.php
index c7ad5d9..445c30c 100644
--- a/src/Normalizer/Value/FieldNormalizerValueInterface.php
+++ b/src/Normalizer/Value/FieldNormalizerValueInterface.php
@@ -2,14 +2,14 @@
 
 namespace Drupal\jsonapi\Normalizer\Value;
 
-use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 
 /**
  * Interface to help normalize fields in compliance with the JSON API spec.
  *
  * @internal
  */
-interface FieldNormalizerValueInterface extends ValueExtractorInterface, RefinableCacheableDependencyInterface {
+interface FieldNormalizerValueInterface extends ValueExtractorInterface, CacheableDependencyInterface {
 
   /**
    * Gets the includes.
@@ -28,26 +28,6 @@ interface FieldNormalizerValueInterface extends ValueExtractorInterface, Refinab
   public function getPropertyType();
 
   /**
-   * Sets the propertyType.
-   *
-   * @param mixed $property_type
-   *   The propertyType to set.
-   */
-  public function setPropertyType($property_type);
-
-  /**
-   * Sets the includes.
-   *
-   * This is used to manually set the nested includes when using the
-   * relationship as a document root in a
-   * /{resource}/{id}/relationships/{fieldName}.
-   *
-   * @param array $includes
-   *   The includes.
-   */
-  public function setIncludes(array $includes);
-
-  /**
    * Computes all the nested includes recursively.
    *
    * @return array
diff --git a/src/Normalizer/Value/NullFieldNormalizerValue.php b/src/Normalizer/Value/NullFieldNormalizerValue.php
index 67a94e6..83e859e 100644
--- a/src/Normalizer/Value/NullFieldNormalizerValue.php
+++ b/src/Normalizer/Value/NullFieldNormalizerValue.php
@@ -2,7 +2,8 @@
 
 namespace Drupal\jsonapi\Normalizer\Value;
 
-use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\jsonapi\Normalizer\CacheableDependencyTrait;
 
 /**
  * Normalizes null fields in accordance with the JSON API specification.
@@ -11,7 +12,7 @@ use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
  */
 class NullFieldNormalizerValue implements FieldNormalizerValueInterface {
 
-  use RefinableCacheableDependencyTrait;
+  use CacheableDependencyTrait;
 
   /**
    * The property type.
@@ -21,24 +22,32 @@ class NullFieldNormalizerValue implements FieldNormalizerValueInterface {
   protected $propertyType;
 
   /**
-   * {@inheritdoc}
+   * Instantiate a FieldNormalizerValue object.
+   *
+   * @param \Drupal\Core\Access\AccessResultInterface $field_access_result
+   *   The field access result.
+   * @param string $property_type
+   *   The property type of the field: 'attributes' or 'relationships'.
    */
-  public function getIncludes() {
-    return [];
+  public function __construct(AccessResultInterface $field_access_result, $property_type) {
+    assert($property_type === 'attributes' || $property_type === 'relationships');
+    $this->setCacheability($field_access_result);
+
+    $this->propertyType = $property_type;
   }
 
   /**
    * {@inheritdoc}
    */
-  public function getPropertyType() {
-    return $this->propertyType;
+  public function getIncludes() {
+    return [];
   }
 
   /**
    * {@inheritdoc}
    */
-  public function setPropertyType($property_type) {
-    $this->propertyType = $property_type;
+  public function getPropertyType() {
+    return $this->propertyType;
   }
 
   /**
@@ -58,13 +67,6 @@ class NullFieldNormalizerValue implements FieldNormalizerValueInterface {
   /**
    * {@inheritdoc}
    */
-  public function setIncludes(array $includes) {
-    // Do nothing.
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   public function getAllIncludes() {
     return NULL;
   }
diff --git a/src/Normalizer/Value/RelationshipItemNormalizerValue.php b/src/Normalizer/Value/RelationshipItemNormalizerValue.php
index a1dbe9d..7df027a 100644
--- a/src/Normalizer/Value/RelationshipItemNormalizerValue.php
+++ b/src/Normalizer/Value/RelationshipItemNormalizerValue.php
@@ -2,17 +2,16 @@
 
 namespace Drupal\jsonapi\Normalizer\Value;
 
-use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
-use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 
 /**
  * Helps normalize relationship items in compliance with the JSON API spec.
  *
  * @internal
  */
-class RelationshipItemNormalizerValue extends FieldItemNormalizerValue implements RefinableCacheableDependencyInterface {
+class RelationshipItemNormalizerValue extends FieldItemNormalizerValue implements ValueExtractorInterface, CacheableDependencyInterface {
 
-  use RefinableCacheableDependencyTrait;
+  use CacheableDependenciesMergerTrait;
 
   /**
    * Resource path.
@@ -22,16 +21,36 @@ class RelationshipItemNormalizerValue extends FieldItemNormalizerValue implement
   protected $resource;
 
   /**
-   * Instantiates a EntityReferenceItemNormalizerValue object.
+   * Included normalized entity, if any.
+   *
+   * @var \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue|null
+   */
+  protected $include;
+
+  /**
+   * Instantiates a RelationshipItemNormalizerValue object.
    *
    * @param array $values
    *   The values.
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $values_cacheability
+   *   The cacheability of the normalized result. This cacheability is not part
+   *   of $values because field items are normalized by Drupal core's
+   *   serialization system, which was never designed with cacheability in mind.
+   *   FieldItemNormalizer::normalize() must catch the out-of-band bubbled
+   *   cacheability and then passes it to this value object.
    * @param string $resource
    *   The resource type of the target entity.
+   * @param \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue|null $include
+   *   The included normalized entity, or NULL.
    */
-  public function __construct(array $values, $resource) {
-    parent::__construct($values);
+  public function __construct(array $values, CacheableDependencyInterface $values_cacheability, $resource, $include) {
+    assert($include === NULL || $include instanceof EntityNormalizerValue || $include instanceof JsonApiDocumentTopLevelNormalizerValue);
+    parent::__construct($values, $values_cacheability);
+    if ($include !== NULL) {
+      $this->setCacheability(static::mergeCacheableDependencies([$include, $values_cacheability]));
+    }
     $this->resource = $resource;
+    $this->include = $include;
   }
 
   /**
@@ -54,13 +73,20 @@ class RelationshipItemNormalizerValue extends FieldItemNormalizerValue implement
   }
 
   /**
-   * Sets the resource.
+   * {@inheritdoc}
+   */
+  public function rasterizeIncludes() {
+    return $this->include->rasterizeValue();
+  }
+
+  /**
+   * Gets the include.
    *
-   * @param string $resource
-   *   The resource to set.
+   * @return \Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue
+   *   The include.
    */
-  public function setResource($resource) {
-    $this->resource = $resource;
+  public function getInclude() {
+    return $this->include;
   }
 
 }
diff --git a/src/Normalizer/Value/RelationshipNormalizerValue.php b/src/Normalizer/Value/RelationshipNormalizerValue.php
index 2ff1143..2549879 100644
--- a/src/Normalizer/Value/RelationshipNormalizerValue.php
+++ b/src/Normalizer/Value/RelationshipNormalizerValue.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\jsonapi\Normalizer\Value;
 
+use Drupal\Core\Access\AccessResultInterface;
+
 /**
  * Helps normalize relationships in compliance with the JSON API spec.
  *
@@ -40,6 +42,8 @@ class RelationshipNormalizerValue extends FieldNormalizerValue {
   /**
    * Instantiate a EntityReferenceNormalizerValue object.
    *
+   * @param \Drupal\Core\Access\AccessResultInterface $relationship_access_result
+   *   The relationship access result.
    * @param RelationshipItemNormalizerValue[] $values
    *   The normalized result.
    * @param int $cardinality
@@ -48,7 +52,7 @@ class RelationshipNormalizerValue extends FieldNormalizerValue {
    *   All the objects and variables needed to generate the links for this
    *   relationship.
    */
-  public function __construct(array $values, $cardinality, array $link_context) {
+  public function __construct(AccessResultInterface $relationship_access_result, array $values, $cardinality, array $link_context) {
     $this->hostEntityId = $link_context['host_entity_id'];
     $this->fieldName = $link_context['field_name'];
     $this->linkManager = $link_context['link_manager'];
@@ -58,7 +62,7 @@ class RelationshipNormalizerValue extends FieldNormalizerValue {
         throw new \RuntimeException(sprintf('Unexpected normalizer item value for this %s.', get_called_class()));
       }
     });
-    parent::__construct($values, $cardinality);
+    parent::__construct($relationship_access_result, $values, $cardinality, 'relationships');
   }
 
   /**
diff --git a/tests/src/Functional/BlockContentTest.php b/tests/src/Functional/BlockContentTest.php
index d4d7c52..a368452 100644
--- a/tests/src/Functional/BlockContentTest.php
+++ b/tests/src/Functional/BlockContentTest.php
@@ -4,6 +4,7 @@ namespace Drupal\Tests\jsonapi\Functional;
 
 use Drupal\block_content\Entity\BlockContent;
 use Drupal\block_content\Entity\BlockContentType;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Url;
 use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
 
@@ -185,25 +186,19 @@ class BlockContentTest extends ResourceTestBase {
       ->addCacheTags(['block_content:1']);
   }
 
-  // @codingStandardsIgnoreStart
   /**
    * {@inheritdoc}
    */
   protected function getExpectedCacheTags() {
-    // @todo Uncomment first line, remove second line in https://www.drupal.org/project/jsonapi/issues/2940342.
-//    return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']);
-    return parent::getExpectedCacheTags();
+    return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']);
   }
 
   /**
    * {@inheritdoc}
    */
   protected function getExpectedCacheContexts() {
-    // @todo Uncomment first line, remove second line in https://www.drupal.org/project/jsonapi/issues/2940342.
-//    return Cache::mergeContexts(['url.site'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
-    return parent::getExpectedCacheContexts();
+    return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['languages:language_interface', 'theme']);
   }
-  // @codingStandardsIgnoreEnd
 
   /**
    * {@inheritdoc}
diff --git a/tests/src/Functional/CommentTest.php b/tests/src/Functional/CommentTest.php
index c4d3d18..b56f768 100644
--- a/tests/src/Functional/CommentTest.php
+++ b/tests/src/Functional/CommentTest.php
@@ -7,6 +7,7 @@ use Drupal\comment\Entity\CommentType;
 use Drupal\comment\Tests\CommentTestTrait;
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Url;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
@@ -248,25 +249,27 @@ class CommentTest extends ResourceTestBase {
     ];
   }
 
-  // @codingStandardsIgnoreStart
   /**
    * {@inheritdoc}
    */
   protected function getExpectedCacheTags() {
-    // @todo Uncomment first line, remove second line in https://www.drupal.org/project/jsonapi/issues/2940342.
-//    return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']);
-    return parent::getExpectedCacheTags();
+    // @todo Remove this when JSON API requires Drupal 8.5 or newer.
+    if (floatval(\Drupal::VERSION) < 8.5) {
+      return parent::getExpectedCacheTags();
+    }
+    return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']);
   }
 
   /**
    * {@inheritdoc}
    */
   protected function getExpectedCacheContexts() {
-    // @todo Uncomment first line, remove second line in https://www.drupal.org/project/jsonapi/issues/2940342.
-//    return Cache::mergeContexts(['languages:language_interface', 'theme'], parent::getExpectedCacheContexts());
-    return parent::getExpectedCacheContexts();
+    // @todo Remove this when JSON API requires Drupal 8.5 or newer.
+    if (floatval(\Drupal::VERSION) < 8.5) {
+      return parent::getExpectedCacheContexts();
+    }
+    return Cache::mergeContexts(['languages:language_interface', 'theme'], parent::getExpectedCacheContexts());
   }
-  // @codingStandardsIgnoreEnd
 
   /**
    * {@inheritdoc}
diff --git a/tests/src/Functional/TermTest.php b/tests/src/Functional/TermTest.php
index a371854..00ff4e5 100644
--- a/tests/src/Functional/TermTest.php
+++ b/tests/src/Functional/TermTest.php
@@ -4,6 +4,7 @@ namespace Drupal\Tests\jsonapi\Functional;
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Url;
 use Drupal\taxonomy\Entity\Term;
 use Drupal\taxonomy\Entity\Vocabulary;
@@ -336,27 +337,28 @@ class TermTest extends ResourceTestBase {
     $this->assertSame($normalization['data']['attributes']['path']['alias'], $updated_normalization['data']['attributes']['path']['alias']);
   }
 
-  // @codingStandardsIgnoreStart
   /**
    * {@inheritdoc}
    */
   protected function getExpectedCacheTags() {
-    // @todo Uncomment first line, remove second line in https://www.drupal.org/project/jsonapi/issues/2940342.
-//    return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text', 'config:filter.settings']);
-    return parent::getExpectedCacheTags();
+    // @todo Remove this when JSON API requires Drupal 8.5 or newer.
+    if (floatval(\Drupal::VERSION) < 8.5) {
+      return parent::getExpectedCacheTags();
+    }
+    return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text', 'config:filter.settings']);
   }
 
   /**
    * {@inheritdoc}
    */
   protected function getExpectedCacheContexts() {
-    // @todo Uncomment first line, remove second line in https://www.drupal.org/project/jsonapi/issues/2940342.
-//    return Cache::mergeContexts(['url.site'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
-    return parent::getExpectedCacheContexts();
+    // @todo Remove this when JSON API requires Drupal 8.5 or newer.
+    if (floatval(\Drupal::VERSION) < 8.5) {
+      return parent::getExpectedCacheContexts();
+    }
+    return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['languages:language_interface', 'theme']);
   }
 
-  // @codingStandardsIgnoreEnd
-
   /**
    * Tests GETting a term with a parent term other than the default <root> (0).
    *
diff --git a/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php b/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
index ce06be1..af6cbea 100644
--- a/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
+++ b/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
@@ -4,6 +4,7 @@ namespace Drupal\Tests\jsonapi\Kernel\Normalizer;
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\file\Entity\File;
 use Drupal\jsonapi\ResourceType\ResourceType;
@@ -48,6 +49,7 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
     'system',
     'taxonomy',
     'text',
+    'filter',
     'user',
     'file',
     'image',
@@ -96,6 +98,7 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
       ['target_bundles' => ['tags']],
       FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
     );
+    $this->createTextField('node', 'article', 'body', 'Body');
 
     $this->createImageField('field_image', 'article');
 
@@ -136,6 +139,10 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
       'title' => 'dummy_title',
       'type' => 'article',
       'uid' => 1,
+      'body' => [
+        'format' => 'plain_text',
+        'value' => $this->randomStringValidate(42),
+      ],
       'field_tags' => [
         ['target_id' => $this->term1->id()],
         ['target_id' => $this->term2->id()],
@@ -680,6 +687,68 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
   }
 
   /**
+   * Ensure that cacheability metadata is properly added.
+   *
+   * @param \Drupal\Core\Cache\CacheableMetadata $expected_metadata
+   *   The expected cacheable metadata.
+   * @param array $fields
+   *   Fields to include in the response, keyed by resource type.
+   * @param array $includes
+   *   Resources paths to include in the response.
+   *
+   * @dataProvider testCacheableMetadataProvider
+   */
+  public function testCacheableMetadata(CacheableMetadata $expected_metadata, $fields = NULL, $includes = NULL) {
+    list($request, $resource_type) = $this->generateProphecies('node', 'article');
+    $actual_metadata = new CacheableMetadata();
+    $context = [
+      'request' => $this->decorateRequest($request, $fields, $includes),
+      'resource_type' => $resource_type,
+      'cacheable_metadata' => $actual_metadata,
+    ];
+    $this->getNormalizer()->normalize(new JsonApiDocumentTopLevel($this->node), 'api_json', $context);
+    $this->assertArraySubset($expected_metadata->getCacheTags(), $actual_metadata->getCacheTags());
+    $this->assertArraySubset($expected_metadata->getCacheContexts(), $actual_metadata->getCacheContexts());
+    $this->assertSame($expected_metadata->getCacheMaxAge(), $actual_metadata->getCacheMaxAge());
+  }
+
+  /**
+   * Provides test cases for asserting cacheable metadata behavior.
+   */
+  public function testCacheableMetadataProvider() {
+    $cacheable_metadata = function ($metadata) {
+      return CacheableMetadata::createFromRenderArray(['#cache' => $metadata]);
+    };
+
+    return [
+      [
+        floatval(\Drupal::VERSION) < 8.5
+          ? $cacheable_metadata([])
+          : $cacheable_metadata(['contexts' => ['languages:language_interface']]),
+        ['node--article' => 'body'],
+      ],
+    ];
+  }
+
+  /**
+   * Decorates a request with sparse fieldsets and includes.
+   */
+  protected function decorateRequest(Request $request, array $fields = NULL, array $includes = NULL) {
+    $parameters = new ParameterBag();
+    $parameters->add($fields ? ['fields' => $fields] : []);
+    $parameters->add($includes ? ['include' => $includes] : []);
+    $request->query = $parameters;
+    return $request;
+  }
+
+  /**
+   * Helper to load the normalizer.
+   */
+  protected function getNormalizer() {
+    return $this->container->get('serializer.normalizer.jsonapi_document_toplevel.jsonapi');
+  }
+
+  /**
    * Generates the prophecies for the mocked entity request.
    *
    * @param string $entity_type_id
diff --git a/tests/src/Unit/Normalizer/Value/EntityNormalizerValueTest.php b/tests/src/Unit/Normalizer/Value/EntityNormalizerValueTest.php
index 50478f1..e184f02 100644
--- a/tests/src/Unit/Normalizer/Value/EntityNormalizerValueTest.php
+++ b/tests/src/Unit/Normalizer/Value/EntityNormalizerValueTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\jsonapi\Unit\Normalizer\Value;
 
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\LinkManager\LinkManager;
@@ -29,17 +30,40 @@ class EntityNormalizerValueTest extends UnitTestCase {
   protected $object;
 
   /**
+   * The cache contexts manager.
+   *
+   * @var \Drupal\Core\Cache\Context\CacheContextsManager|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $cacheContextsManager;
+
+  /**
    * {@inheritdoc}
    */
   protected function setUp() {
     parent::setUp();
+
+    $this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->cacheContextsManager->method('assertValidTokens')->willReturn(TRUE);
+
+    $container = new ContainerBuilder();
+    $container->set('cache_contexts_manager', $this->cacheContextsManager);
+    \Drupal::setContainer($container);
+
     $field1 = $this->prophesize(FieldNormalizerValueInterface::class);
     $field1->getIncludes()->willReturn([]);
     $field1->getPropertyType()->willReturn('attributes');
     $field1->rasterizeValue()->willReturn('dummy_title');
+    $field1->getCacheContexts()->willReturn(['ccbar']);
+    $field1->getCacheTags()->willReturn(['ctbar']);
+    $field1->getCacheMaxAge()->willReturn(20);
     $field2 = $this->prophesize(RelationshipNormalizerValue::class);
     $field2->getPropertyType()->willReturn('relationships');
     $field2->rasterizeValue()->willReturn(['data' => ['type' => 'node', 'id' => 2]]);
+    $field2->getCacheContexts()->willReturn(['ccbaz']);
+    $field2->getCacheTags()->willReturn(['ctbaz']);
+    $field2->getCacheMaxAge()->willReturn(25);
     $included[] = $this->prophesize(JsonApiDocumentTopLevelNormalizerValue::class);
     $included[0]->getIncludes()->willReturn([]);
     $included[0]->rasterizeValue()->willReturn([
@@ -81,6 +105,9 @@ class EntityNormalizerValueTest extends UnitTestCase {
     $entity->isNew()->willReturn(FALSE);
     $entity->getEntityTypeId()->willReturn('node');
     $entity->bundle()->willReturn('article');
+    $entity->getCacheContexts()->willReturn(['ccfoo']);
+    $entity->getCacheTags()->willReturn(['ctfoo']);
+    $entity->getCacheMaxAge()->willReturn(15);
     $link_manager = $this->prophesize(LinkManager::class);
     $link_manager
       ->getEntityLink(Argument::any(), Argument::any(), Argument::type('array'), Argument::type('string'))
@@ -101,6 +128,15 @@ class EntityNormalizerValueTest extends UnitTestCase {
   }
 
   /**
+   * @covers ::__construct
+   */
+  public function testCacheability() {
+    $this->assertSame(['ccbar', 'ccbaz', 'ccfoo'], $this->object->getCacheContexts());
+    $this->assertSame(['ctbar', 'ctbaz', 'ctfoo'], $this->object->getCacheTags());
+    $this->assertSame(15, $this->object->getCacheMaxAge());
+  }
+
+  /**
    * @covers ::rasterizeValue
    */
   public function testRasterizeValue() {
diff --git a/tests/src/Unit/Normalizer/Value/FieldItemNormalizerValueTest.php b/tests/src/Unit/Normalizer/Value/FieldItemNormalizerValueTest.php
index acfac6c..17dcbfd 100644
--- a/tests/src/Unit/Normalizer/Value/FieldItemNormalizerValueTest.php
+++ b/tests/src/Unit/Normalizer/Value/FieldItemNormalizerValueTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\jsonapi\Unit\Normalizer\Value;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
 use Drupal\Tests\UnitTestCase;
 
@@ -18,7 +19,7 @@ class FieldItemNormalizerValueTest extends UnitTestCase {
    * @dataProvider rasterizeValueProvider
    */
   public function testRasterizeValue($values, $expected) {
-    $object = new FieldItemNormalizerValue($values);
+    $object = new FieldItemNormalizerValue($values, new CacheableMetadata());
     $this->assertEquals($expected, $object->rasterizeValue());
   }
 
@@ -34,10 +35,10 @@ class FieldItemNormalizerValueTest extends UnitTestCase {
       [
         [
           'lorem' => [
-            'ipsum' => new FieldItemNormalizerValue([
+            'ipsum' => [
               'dolor' => 'sid',
-              'amet' => new FieldItemNormalizerValue(['value' => 'ra']),
-            ]),
+              'amet' => 'ra',
+            ],
           ],
         ],
         ['ipsum' => ['dolor' => 'sid', 'amet' => 'ra']],
diff --git a/tests/src/Unit/Normalizer/Value/FieldNormalizerValueTest.php b/tests/src/Unit/Normalizer/Value/FieldNormalizerValueTest.php
index f247ad3..e5dac5f 100644
--- a/tests/src/Unit/Normalizer/Value/FieldNormalizerValueTest.php
+++ b/tests/src/Unit/Normalizer/Value/FieldNormalizerValueTest.php
@@ -2,8 +2,11 @@
 
 namespace Drupal\Tests\jsonapi\Unit\Normalizer\Value;
 
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
 use Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue;
+use Drupal\jsonapi\Normalizer\Value\RelationshipItemNormalizerValue;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -15,12 +18,39 @@ use Drupal\Tests\UnitTestCase;
 class FieldNormalizerValueTest extends UnitTestCase {
 
   /**
+   * The cache contexts manager.
+   *
+   * @var \Drupal\Core\Cache\Context\CacheContextsManager|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $cacheContextsManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->cacheContextsManager->method('assertValidTokens')->willReturn(TRUE);
+
+    $container = new ContainerBuilder();
+    $container->set('cache_contexts_manager', $this->cacheContextsManager);
+    \Drupal::setContainer($container);
+  }
+
+  /**
    * @covers ::rasterizeValue
+   * @covers ::__construct
    * @dataProvider rasterizeValueProvider
    */
   public function testRasterizeValue($values, $cardinality, $expected) {
-    $object = new FieldNormalizerValue($values, $cardinality);
+    $object = new FieldNormalizerValue(AccessResult::allowed()->cachePerUser()->addCacheTags(['field:foo']), $values, $cardinality, 'attributes');
     $this->assertEquals($expected, $object->rasterizeValue());
+    $this->assertSame(['ccfoo', 'user'], $object->getCacheContexts());
+    $this->assertSame(['ctfoo', 'field:foo'], $object->getCacheTags());
+    $this->assertSame(15, $object->getCacheMaxAge());
   }
 
   /**
@@ -30,7 +60,9 @@ class FieldNormalizerValueTest extends UnitTestCase {
     $uuid_raw = '4ae99eec-8b0e-41f7-9400-fbd65c174902';
     $uuid_value = $this->prophesize(FieldItemNormalizerValue::class);
     $uuid_value->rasterizeValue()->willReturn('4ae99eec-8b0e-41f7-9400-fbd65c174902');
-    $uuid_value->getInclude()->willReturn(NULL);
+    $uuid_value->getCacheContexts()->willReturn(['ccfoo']);
+    $uuid_value->getCacheTags()->willReturn(['ctfoo']);
+    $uuid_value->getCacheMaxAge()->willReturn(15);
     return [
       [[$uuid_value->reveal()], 1, $uuid_raw],
       [
@@ -48,11 +80,14 @@ class FieldNormalizerValueTest extends UnitTestCase {
    * @covers ::rasterizeIncludes
    */
   public function testRasterizeIncludes() {
-    $value = $this->prophesize(FieldItemNormalizerValue::class);
+    $value = $this->prophesize(RelationshipItemNormalizerValue::class);
     $include = $this->prophesize('\Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue');
     $include->rasterizeValue()->willReturn('Lorem');
+    $value->getCacheContexts()->willReturn(['ccfoo']);
+    $value->getCacheTags()->willReturn(['ctfoo']);
+    $value->getCacheMaxAge()->willReturn(15);
     $value->getInclude()->willReturn($include->reveal());
-    $object = new FieldNormalizerValue([$value->reveal()], 1);
+    $object = new FieldNormalizerValue(AccessResult::allowed(), [$value->reveal()], 1, 'attributes');
     $this->assertEquals(['Lorem'], $object->rasterizeIncludes());
   }
 
diff --git a/tests/src/Unit/Normalizer/Value/RelationshipItemNormalizerValueTest.php b/tests/src/Unit/Normalizer/Value/RelationshipItemNormalizerValueTest.php
index a72a912..8f400c1 100644
--- a/tests/src/Unit/Normalizer/Value/RelationshipItemNormalizerValueTest.php
+++ b/tests/src/Unit/Normalizer/Value/RelationshipItemNormalizerValueTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\jsonapi\Unit\Normalizer\Value;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\Normalizer\Value\RelationshipItemNormalizerValue;
 use Drupal\Tests\UnitTestCase;
@@ -19,7 +20,7 @@ class RelationshipItemNormalizerValueTest extends UnitTestCase {
    * @dataProvider rasterizeValueProvider
    */
   public function testRasterizeValue($values, $entity_type_id, $bundle, $expected) {
-    $object = new RelationshipItemNormalizerValue($values, new ResourceType($entity_type_id, $bundle, NULL));
+    $object = new RelationshipItemNormalizerValue($values, new CacheableMetadata(), new ResourceType($entity_type_id, $bundle, NULL), NULL);
     $this->assertEquals($expected, $object->rasterizeValue());
   }
 
diff --git a/tests/src/Unit/Normalizer/Value/RelationshipNormalizerValueTest.php b/tests/src/Unit/Normalizer/Value/RelationshipNormalizerValueTest.php
index ea74c5b..bc72ee4 100644
--- a/tests/src/Unit/Normalizer/Value/RelationshipNormalizerValueTest.php
+++ b/tests/src/Unit/Normalizer/Value/RelationshipNormalizerValueTest.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\Tests\jsonapi\Unit\Normalizer\Value;
 
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\LinkManager\LinkManager;
 use Drupal\jsonapi\Normalizer\Value\RelationshipItemNormalizerValue;
@@ -19,10 +22,33 @@ use Prophecy\Argument;
 class RelationshipNormalizerValueTest extends UnitTestCase {
 
   /**
+   * The cache contexts manager.
+   *
+   * @var \Drupal\Core\Cache\Context\CacheContextsManager|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $cacheContextsManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
+      ->disableOriginalConstructor()
+      ->getMock();
+    $this->cacheContextsManager->method('assertValidTokens')->willReturn(TRUE);
+
+    $container = new ContainerBuilder();
+    $container->set('cache_contexts_manager', $this->cacheContextsManager);
+    \Drupal::setContainer($container);
+  }
+
+  /**
    * @covers ::rasterizeValue
    * @dataProvider rasterizeValueProvider
    */
-  public function testRasterizeValue($values, $cardinality, $expected) {
+  public function testRasterizeValue($values, $cardinality, $expected, CacheableMetadata $expected_cacheability) {
     $link_manager = $this->prophesize(LinkManager::class);
     $link_manager
       ->getEntityLink(Argument::any(), Argument::any(), Argument::type('array'), Argument::type('string'))
@@ -31,13 +57,16 @@ class RelationshipNormalizerValueTest extends UnitTestCase {
     $resource_type->setRelatableResourceTypes([
       'ipsum' => [$resource_type],
     ]);
-    $object = new RelationshipNormalizerValue($values, $cardinality, [
+    $object = new RelationshipNormalizerValue(AccessResult::allowed()->cachePerUser()->addCacheTags(['relationship:foo']), $values, $cardinality, [
       'link_manager' => $link_manager->reveal(),
       'host_entity_id' => 'lorem',
       'resource_type' => $resource_type,
       'field_name' => 'ipsum',
     ]);
     $this->assertEquals($expected, $object->rasterizeValue());
+    $this->assertSame($expected_cacheability->getCacheContexts(), $object->getCacheContexts());
+    $this->assertSame($expected_cacheability->getCacheTags(), $object->getCacheTags());
+    $this->assertSame($expected_cacheability->getCacheMaxAge(), $object->getCacheMaxAge());
   }
 
   /**
@@ -48,9 +77,15 @@ class RelationshipNormalizerValueTest extends UnitTestCase {
     $uid1 = $this->prophesize(RelationshipItemNormalizerValue::class);
     $uid1->rasterizeValue()->willReturn(['type' => 'user', 'id' => $uid_raw++]);
     $uid1->getInclude()->willReturn(NULL);
+    $uid1->getCacheContexts()->willReturn(['ccfoo']);
+    $uid1->getCacheTags()->willReturn(['ctfoo']);
+    $uid1->getCacheMaxAge()->willReturn(15);
     $uid2 = $this->prophesize(RelationshipItemNormalizerValue::class);
     $uid2->rasterizeValue()->willReturn(['type' => 'user', 'id' => $uid_raw]);
     $uid2->getInclude()->willReturn(NULL);
+    $uid2->getCacheContexts()->willReturn(['ccbar']);
+    $uid2->getCacheTags()->willReturn(['ctbar']);
+    $uid2->getCacheMaxAge()->willReturn(10);
     $links = [
       'self' => 'dummy_entity_link',
       'related' => 'dummy_entity_link',
@@ -60,6 +95,10 @@ class RelationshipNormalizerValueTest extends UnitTestCase {
         'data' => ['type' => 'user', 'id' => 1],
         'links' => $links,
       ],
+        (new CacheableMetadata())
+          ->setCacheContexts(['ccfoo', 'user'])
+          ->setCacheTags(['ctfoo', 'relationship:foo'])
+          ->setCacheMaxAge(15)
       ],
       [
         [$uid1->reveal(), $uid2->reveal()], 2, [
@@ -69,6 +108,10 @@ class RelationshipNormalizerValueTest extends UnitTestCase {
           ],
           'links' => $links,
         ],
+        (new CacheableMetadata())
+          ->setCacheContexts(['ccbar', 'ccfoo', 'user'])
+          ->setCacheTags(['ctbar', 'ctfoo', 'relationship:foo'])
+          ->setCacheMaxAge(10)
       ],
     ];
   }
