 .../jsonapi_test_computed_field.info.yml           |  9 +++
 .../jsonapi_test_computed_field.module             | 43 +++++++++++
 .../src/JsonApiComputedFieldItemList.php           | 30 ++++++++
 .../src/Plugin/DataType/CacheableStringData.php    | 24 +++++++
 .../Plugin/Field/FieldType/CacheableStringItem.php | 37 ++++++++++
 tests/src/Functional/EntityTestTest.php            | 28 +++++++-
 .../Serializer/ComputedFieldSerializerTest.php     | 83 ++++++++++++++++++++++
 7 files changed, 253 insertions(+), 1 deletion(-)

diff --git a/tests/modules/jsonapi_test_computed_field/jsonapi_test_computed_field.info.yml b/tests/modules/jsonapi_test_computed_field/jsonapi_test_computed_field.info.yml
new file mode 100644
index 0000000..5c61ed6
--- /dev/null
+++ b/tests/modules/jsonapi_test_computed_field/jsonapi_test_computed_field.info.yml
@@ -0,0 +1,9 @@
+name: 'JSON API computed field test'
+description: 'JSON API test for computed field normalization'
+type: module
+package: Testing
+core: 8.x
+dependencies:
+  - drupal:entity_test
+  - jsonapi:jsonapi
+  - drupal:field
diff --git a/tests/modules/jsonapi_test_computed_field/jsonapi_test_computed_field.module b/tests/modules/jsonapi_test_computed_field/jsonapi_test_computed_field.module
new file mode 100644
index 0000000..2cbca3d
--- /dev/null
+++ b/tests/modules/jsonapi_test_computed_field/jsonapi_test_computed_field.module
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Module file for JSON API computed field test.
+ */
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\jsonapi_test_computed_field\JsonApiComputedFieldItemList;
+
+/**
+ * Implements hook_entity_base_field_info().
+ */
+function jsonapi_test_computed_field_entity_base_field_info(EntityTypeInterface $entity_type) {
+  $fields = [];
+  if ($entity_type->id() === 'entity_test') {
+    $fields['jsonapi_test_computed_field'] = BaseFieldDefinition::create('cacheable_string')
+      ->setLabel(t('JSON API test computed field'))
+      ->setDescription(t('JSON API test for computed field normalization.'))
+      ->setComputed(TRUE)
+      ->setClass(JsonApiComputedFieldItemList::class)
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', FALSE)
+      ->setReadOnly(FALSE)
+      ->setInternal(FALSE)
+      ->setDisplayOptions('view', [
+        'label' => 'hidden',
+        'region' => 'hidden',
+        'weight' => -5,
+      ])
+      ->setDisplayOptions('form', [
+        'label' => 'hidden',
+        'region' => 'hidden',
+        'weight' => -5,
+      ])
+      ->setTargetEntityTypeId('entity_test')
+      ->setTargetBundle('entity_test')
+      ->setName('jsonapi_test_computed_field');
+  }
+
+  return $fields;
+}
diff --git a/tests/modules/jsonapi_test_computed_field/src/JsonApiComputedFieldItemList.php b/tests/modules/jsonapi_test_computed_field/src/JsonApiComputedFieldItemList.php
new file mode 100644
index 0000000..bf1001d
--- /dev/null
+++ b/tests/modules/jsonapi_test_computed_field/src/JsonApiComputedFieldItemList.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\jsonapi_test_computed_field;
+
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Field\FieldItemList;
+use Drupal\Core\TypedData\ComputedItemListTrait;
+
+/**
+ * Item list class for jsonapi_test_computed_field.
+ */
+class JsonApiComputedFieldItemList extends FieldItemList {
+
+  use ComputedItemListTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function computeValue() {
+    /** @var \Drupal\jsonapi_test_computed_field\Plugin\Field\FieldType\CacheableStringItem $item */
+    $item = $this->createItem(0, 'jsonapi_test_computed_field');
+    $cacheability = (new CacheableMetadata())
+      ->setCacheContexts(['url.query_args:jsonapi_test_computed_field'])
+      ->setCacheTags(['field:jsonapi_test_computed_field'])
+      ->setCacheMaxAge(800);
+    $item->get('value')->addCacheableDependency($cacheability);
+    $this->list[0] = $item;
+  }
+
+}
diff --git a/tests/modules/jsonapi_test_computed_field/src/Plugin/DataType/CacheableStringData.php b/tests/modules/jsonapi_test_computed_field/src/Plugin/DataType/CacheableStringData.php
new file mode 100644
index 0000000..ad32cec
--- /dev/null
+++ b/tests/modules/jsonapi_test_computed_field/src/Plugin/DataType/CacheableStringData.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\jsonapi_test_computed_field\Plugin\DataType;
+
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+use Drupal\Core\TypedData\TypedData;
+
+/**
+ * The string data type with cacheablity metadata.
+ *
+ * The plain value of a string is a regular PHP string. For setting the value
+ * any PHP variable that casts to a string may be passed.
+ *
+ * @DataType(
+ *   id = "cacheable_string",
+ *   label = @Translation("Cacheable String")
+ * )
+ */
+class CacheableStringData extends TypedData implements RefinableCacheableDependencyInterface {
+
+  use RefinableCacheableDependencyTrait;
+
+}
diff --git a/tests/modules/jsonapi_test_computed_field/src/Plugin/Field/FieldType/CacheableStringItem.php b/tests/modules/jsonapi_test_computed_field/src/Plugin/Field/FieldType/CacheableStringItem.php
new file mode 100644
index 0000000..51628d1
--- /dev/null
+++ b/tests/modules/jsonapi_test_computed_field/src/Plugin/Field/FieldType/CacheableStringItem.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\jsonapi_test_computed_field\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\StringItem;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * Defines the 'string' entity field type with cacheablity metadata.
+ *
+ * @FieldType(
+ *   id = "cacheable_string",
+ *   label = @Translation("Text (plain with cacheablity)"),
+ *   description = @Translation("A field containing a plain string value and cacheablity metadata."),
+ *   category = @Translation("Text"),
+ *   no_ui = TRUE,
+ *   default_widget = "string_textfield",
+ *   default_formatter = "string"
+ * )
+ */
+class CacheableStringItem extends StringItem {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties['value'] = DataDefinition::create('cacheable_string')
+      ->setLabel(new TranslatableMarkup('Text value'))
+      ->setSetting('case_sensitive', $field_definition->getSetting('case_sensitive'))
+      ->setRequired(TRUE);
+
+    return $properties;
+  }
+
+}
diff --git a/tests/src/Functional/EntityTestTest.php b/tests/src/Functional/EntityTestTest.php
index 49a031b..730e8c0 100644
--- a/tests/src/Functional/EntityTestTest.php
+++ b/tests/src/Functional/EntityTestTest.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Tests\jsonapi\Functional;
 
+use Drupal\Core\Cache\Cache;
 use Drupal\Core\Url;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
@@ -19,7 +20,7 @@ class EntityTestTest extends ResourceTestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['entity_test'];
+  public static $modules = ['entity_test', 'jsonapi_test_computed_field'];
 
   /**
    * {@inheritdoc}
@@ -115,6 +116,7 @@ class EntityTestTest extends ResourceTestBase {
         'attributes' => [
           'created' => (new \DateTime())->setTimestamp($this->entity->get('created')->value)->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
           'field_test_text' => NULL,
+          'jsonapi_test_computed_field' => 'jsonapi_test_computed_field',
           'langcode' => 'en',
           'name' => 'Llama',
           'entity_test_type' => 'entity_test',
@@ -178,4 +180,28 @@ class EntityTestTest extends ResourceTestBase {
     ]));
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedCacheTags(array $sparse_fieldset = NULL) {
+    $tags = parent::getExpectedCacheTags($sparse_fieldset);
+    // @see jsonapi_test_computed_field_entity_base_field_info()
+    if ($sparse_fieldset === NULL || in_array('jsonapi_test_computed_field', $sparse_fieldset)) {
+      $tags = Cache::mergeTags($tags, ['field:jsonapi_test_computed_field']);
+    }
+    return $tags;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedCacheContexts(array $sparse_fieldset = NULL) {
+    $contexts = parent::getExpectedCacheContexts($sparse_fieldset);
+    // @see jsonapi_test_computed_field_entity_base_field_info()
+    if ($sparse_fieldset === NULL || in_array('jsonapi_test_computed_field', $sparse_fieldset)) {
+      $contexts = Cache::mergeContexts($contexts, ['url.query_args:jsonapi_test_computed_field']);
+    }
+    return $contexts;
+  }
+
 }
diff --git a/tests/src/Kernel/Serializer/ComputedFieldSerializerTest.php b/tests/src/Kernel/Serializer/ComputedFieldSerializerTest.php
new file mode 100644
index 0000000..66633fb
--- /dev/null
+++ b/tests/src/Kernel/Serializer/ComputedFieldSerializerTest.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\Tests\jsonapi\Kernel\Serializer;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue;
+use Drupal\Tests\jsonapi\Kernel\JsonapiKernelTestBase;
+use Drupal\user\Entity\User;
+
+/**
+ * Tests the JSON API serializer.
+ *
+ * @coversClass \Drupal\jsonapi\Serializer\Serializer
+ * @group jsonapi
+ *
+ * @internal
+ */
+class ComputedFieldSerializerTest extends JsonapiKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'system',
+    'field',
+    'text',
+    'user',
+    'entity_test',
+    'serialization',
+    'jsonapi_test_computed_field',
+  ];
+
+  /**
+   * An entity for testing.
+   *
+   * @var \Drupal\entity_test\Entity\EntityTest
+   */
+  protected $entity;
+
+  /**
+   * The subject under test.
+   *
+   * @var \Drupal\jsonapi\Serializer\Serializer
+   */
+  protected $sut;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    // Add the entity schemas.
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('entity_test');
+    // Add the additional table schemas.
+    $this->installSchema('system', ['sequences']);
+    $this->user = User::create([
+      'name' => $this->randomString(),
+      'status' => 1,
+    ]);
+    $this->user->save();
+    $this->container->setAlias('sut', 'jsonapi.serializer_do_not_use_removal_imminent');
+    $this->sut = $this->container->get('sut');
+    $this->entity = EntityTest::create([
+      'name' => 'Test Entity for Computed Field',
+      'type' => 'entity_test',
+    ]);
+    $this->entity->save();
+  }
+
+  /**
+   * Tests computed field cacheablity metadata.
+   */
+  public function testComputedFieldCacheablity() {
+    $context = ['account' => $this->user];
+    $value = $this->sut->normalize($this->entity, 'api_json', $context);
+    $this->assertInstanceOf(EntityNormalizerValue::class, $value);
+    $this->assertContains('url.query_args:jsonapi_test_computed_field', $value->getCacheContexts());
+    $this->assertContains('field:jsonapi_test_computed_field', $value->getCacheTags());
+    $this->assertSame(800, $value->getCacheMaxAge());
+  }
+
+}
