 src/EntityToJsonApi.php                            |  3 +-
 src/EventSubscriber/ResourceResponseSubscriber.php |  3 +-
 src/Normalizer/EntityNormalizer.php                |  3 +-
 .../JsonApiDocumentTopLevelNormalizer.php          |  4 +-
 src/Normalizer/RelationshipItemNormalizer.php      |  4 +-
 .../JsonApiDocumentTopLevelNormalizerTest.php      | 78 ++++++++++++++++++++--
 6 files changed, 83 insertions(+), 12 deletions(-)

diff --git a/src/EntityToJsonApi.php b/src/EntityToJsonApi.php
index 09f97be..3c3da16 100644
--- a/src/EntityToJsonApi.php
+++ b/src/EntityToJsonApi.php
@@ -8,6 +8,7 @@ use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
 use Drupal\jsonapi\Serializer\Serializer;
+use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
 use Symfony\Component\HttpFoundation\Request;
 
 /**
@@ -102,7 +103,7 @@ class EntityToJsonApi {
     $request = Request::create($path, 'GET');
     return [
       'account' => $this->currentUser,
-      'cacheable_metadata' => new CacheableMetadata(),
+      CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => new CacheableMetadata(),
       'resource_type' => $this->resourceTypeRepository->get(
         $entity->getEntityTypeId(),
         $entity->bundle()
diff --git a/src/EventSubscriber/ResourceResponseSubscriber.php b/src/EventSubscriber/ResourceResponseSubscriber.php
index 8fddd81..303e487 100644
--- a/src/EventSubscriber/ResourceResponseSubscriber.php
+++ b/src/EventSubscriber/ResourceResponseSubscriber.php
@@ -10,6 +10,7 @@ use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\jsonapi\ResourceResponse;
 use Drupal\schemata\SchemaFactory;
+use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
 use JsonSchema\Validator;
 use Psr\Log\LoggerInterface;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
@@ -215,7 +216,7 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
         // updating the response object's cacheability.
         return $serializer->serialize($data, $format, [
           'request' => $request,
-          'cacheable_metadata' => $response->getCacheableMetadata(),
+          CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => $response->getCacheableMetadata(),
         ]);
       };
       $output = $this->renderer->executeInRenderContext($context, $render_function);
diff --git a/src/Normalizer/EntityNormalizer.php b/src/Normalizer/EntityNormalizer.php
index bb07c12..775c7f0 100644
--- a/src/Normalizer/EntityNormalizer.php
+++ b/src/Normalizer/EntityNormalizer.php
@@ -13,6 +13,7 @@ use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\LinkManager\LinkManager;
 use Drupal\jsonapi\Normalizer\Value\NullFieldNormalizerValue;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
+use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
 use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
 use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
@@ -216,7 +217,7 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
     /* @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);
+    $context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheableDependency($access);
     if ($field instanceof AccessibleInterface && !$access->isAllowed()) {
       return (new NullFieldNormalizerValue())->addCacheableDependency($access);
     }
diff --git a/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
index 1f33db5..0f893bc 100644
--- a/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
+++ b/src/Normalizer/JsonApiDocumentTopLevelNormalizer.php
@@ -197,8 +197,8 @@ class JsonApiDocumentTopLevelNormalizer extends NormalizerBase implements Denorm
       $context['resource_type'] = $this->currentContext->getResourceType();
     }
     $value_extractor = $this->buildNormalizerValue($object->getData(), $format, $context);
-    if (!empty($context['cacheable_metadata'])) {
-      $context['cacheable_metadata']->addCacheableDependency($value_extractor);
+    if (!empty($context[static::SERIALIZATION_CONTEXT_CACHEABILITY])) {
+      $context[static::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheableDependency($value_extractor);
     }
     $normalized = $value_extractor->rasterizeValue();
     $included = array_filter($value_extractor->rasterizeIncludes());
diff --git a/src/Normalizer/RelationshipItemNormalizer.php b/src/Normalizer/RelationshipItemNormalizer.php
index 93bc6af..e95e26b 100644
--- a/src/Normalizer/RelationshipItemNormalizer.php
+++ b/src/Normalizer/RelationshipItemNormalizer.php
@@ -83,8 +83,8 @@ class RelationshipItemNormalizer extends FieldItemNormalizer {
       // 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);
+      if (!empty($context[static::SERIALIZATION_CONTEXT_CACHEABILITY])) {
+        $context[static::SERIALIZATION_CONTEXT_CACHEABILITY]->addCacheableDependency($normalizer_value);
       }
     }
     return $normalizer_value;
diff --git a/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php b/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
index ce06be1..fb35422 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;
@@ -12,6 +13,7 @@ use Drupal\jsonapi\Resource\JsonApiDocumentTopLevel;
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
 use Drupal\jsonapi\ResourceResponse;
+use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
 use Drupal\taxonomy\Entity\Term;
 use Drupal\taxonomy\Entity\Vocabulary;
 use Drupal\Tests\image\Kernel\ImageFieldCreationTrait;
@@ -48,6 +50,7 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
     'system',
     'taxonomy',
     'text',
+    'filter',
     'user',
     'file',
     'image',
@@ -96,6 +99,7 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
       ['target_bundles' => ['tags']],
       FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
     );
+    $this->createTextField('node', 'article', 'body', 'Body');
 
     $this->createImageField('field_image', 'article');
 
@@ -136,6 +140,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()],
@@ -220,7 +228,7 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
         [
           'request' => $request,
           'resource_type' => $resource_type,
-          'cacheable_metadata' => $response->getCacheableMetadata(),
+          CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => $response->getCacheableMetadata(),
         ]
       );
 
@@ -319,7 +327,7 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
         [
           'request' => $request,
           'resource_type' => $resource_type,
-          'cacheable_metadata' => $response->getCacheableMetadata(),
+          CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => $response->getCacheableMetadata(),
         ]
       );
     $this->assertSame($normalized['data']['attributes']['name'], 'user1');
@@ -358,7 +366,7 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
         [
           'request' => $request,
           'resource_type' => $resource_type,
-          'cacheable_metadata' => $response->getCacheableMetadata(),
+          CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => $response->getCacheableMetadata(),
         ]
       );
     $this->assertStringMatchesFormat($this->node->uuid(), $normalized['data']['id']);
@@ -400,7 +408,7 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
         [
           'request' => $request,
           'resource_type' => $resource_type,
-          'cacheable_metadata' => $response->getCacheableMetadata(),
+          CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => $response->getCacheableMetadata(),
           'data_wrapper' => 'errors',
         ]
       );
@@ -433,7 +441,7 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
       ->normalize($document_wrapper->reveal(), 'api_json', [
         'request' => $request,
         'resource_type' => $resource_type,
-        'cacheable_metadata' => $response->getCacheableMetadata(),
+        CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => $response->getCacheableMetadata(),
       ]);
     $this->assertTrue(empty($normalized['data']['attributes']['type']));
     $this->assertTrue(!empty($normalized['data']['attributes']['uuid']));
@@ -680,6 +688,66 @@ 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,
+      CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => $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 [
+      [
+        $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
