diff --git a/core/lib/Drupal/Core/Field/FieldConfigBase.php b/core/lib/Drupal/Core/Field/FieldConfigBase.php
index e53e84b6c8..d1a0539dd5 100644
--- a/core/lib/Drupal/Core/Field/FieldConfigBase.php
+++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php
@@ -6,12 +6,14 @@
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
+use Drupal\Core\TypedData\ExposableDataDefinitionTrait;
 
 /**
  * Base class for configurable field definitions.
  */
 abstract class FieldConfigBase extends ConfigEntityBase implements FieldConfigInterface {
 
+  use ExposableDataDefinitionTrait;
   /**
    * The field ID.
    *
diff --git a/core/lib/Drupal/Core/TypedData/DataDefinition.php b/core/lib/Drupal/Core/TypedData/DataDefinition.php
index 52a4394cd7..6e0f936a8f 100644
--- a/core/lib/Drupal/Core/TypedData/DataDefinition.php
+++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php
@@ -9,6 +9,8 @@ class DataDefinition implements DataDefinitionInterface, \ArrayAccess {
 
   use TypedDataTrait;
 
+  use ExposableDataDefinitionTrait;
+
   /**
    * The array holding values for all definition keys.
    *
diff --git a/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
index 64d779c317..7ea68e47e6 100644
--- a/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
+++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
@@ -218,4 +218,23 @@ public function getConstraint($constraint_name);
    */
   public function addConstraint($constraint_name, $options = NULL);
 
+  /**
+   * Sets the whether the data value should be exposed.
+   *
+   * @param bool $exposed
+   *   Whether the data value is exposed.
+   *
+   * @return static
+   *   The object itself for chaining.
+   */
+  public function setExposed($exposed = TRUE);
+
+  /**
+   * Determines whether the data value is exposed.
+   *
+   * @return bool
+   *   Whether the data value is exposed.
+   */
+  public function isExposed();
+
 }
diff --git a/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php
new file mode 100644
index 0000000000..94785731ca
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php
@@ -0,0 +1,37 @@
+<?php
+
+
+namespace Drupal\Core\TypedData;
+
+/**
+ * Exposable property methods.
+ *
+ * Methods are implemented for \Drupal\Core\TypedData\DataDefinitionInterface.
+ *
+ * @see \Drupal\Core\TypedData\DataDefinitionInterface
+ */
+trait ExposableDataDefinitionTrait {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isExposed() {
+    // Respect the definition, otherwise default to TRUE for non-computed
+    // fields.
+    if (isset($this->definition['exposed'])) {
+      return $this->definition['exposed'];
+    }
+    else {
+      return !$this->isComputed();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setExposed($exposed = TRUE) {
+    $this->definition['exposed'] = $exposed;
+    return $this;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/TypedData/TypedDataHelper.php b/core/lib/Drupal/Core/TypedData/TypedDataHelper.php
new file mode 100644
index 0000000000..1df10eb252
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/TypedDataHelper.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\Core\TypedData;
+
+/**
+ * Helper class for Typed Data.
+ *
+ * @ingroup typed_data
+ */
+class TypedDataHelper {
+
+  /**
+   * Gets an array exposed properties from a complex data object.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $data
+   *   The complex data object.
+   *
+   * @return \Drupal\Core\TypedData\TypedDataInterface[]
+   *   The exposed properties.
+   */
+  public static function getExposedProperties(ComplexDataInterface $data) {
+    return array_filter($data->getProperties(TRUE), function (TypedDataInterface $property) {
+      return $property->getDataDefinition()->isExposed();
+    });
+  }
+
+}
diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
index b437ae765b..4a44b03409 100644
--- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
@@ -3,6 +3,7 @@
 namespace Drupal\hal\Normalizer;
 
 use Drupal\Core\Field\FieldItemInterface;
+use Drupal\serialization\Normalizer\ComplexDataPropertiesNormalizerTrait;
 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
 
 /**
@@ -10,6 +11,8 @@
  */
 class FieldItemNormalizer extends NormalizerBase {
 
+  use ComplexDataPropertiesNormalizerTrait;
+
   /**
    * The interface or class that this Normalizer supports.
    *
@@ -87,13 +90,7 @@ protected function constructValue($data, $context) {
    *   An array of field item values, keyed by property name.
    */
   protected function normalizedFieldValues(FieldItemInterface $field_item, $format, array $context) {
-    $denormalized = [];
-    // We normalize each individual property, so each can do their own casting,
-    // if needed.
-    /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
-    foreach ($field_item as $property_name => $property) {
-      $denormalized[$property_name] = $this->serializer->normalize($property, $format, $context);
-    }
+    $denormalized = $this->normalizeProperties($field_item, $format, $context);
 
     if (isset($context['langcode'])) {
       $denormalized['lang'] = $context['langcode'];
diff --git a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
index df77281f37..7dc6585283 100644
--- a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
+++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\rest\EventSubscriber;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Cache\CacheableResponse;
 use Drupal\Core\Cache\CacheableResponseInterface;
 use Drupal\Core\Render\RenderContext;
@@ -126,11 +127,18 @@ public function getResponseFormat(RouteMatchInterface $route_match, Request $req
   /**
    * Renders a resource response body.
    *
-   * Serialization can invoke rendering (e.g., generating URLs), but the
-   * serialization API does not provide a mechanism to collect the
-   * bubbleable metadata associated with that (e.g., language and other
-   * contexts), so instead, allow those to "leak" and collect them here in
-   * a render context.
+   * During serialization, encoders and normalizers are able to explicitly
+   * bubble cacheability metadata via the 'cacheability' key-value pair in the
+   * received context. This bubbled cacheability metadata will be applied to the
+   * the response.
+   *
+   * In prior versions of Drupal 8, we allowed implicit bubbling of cacheability
+   * metadata because there was no explicit cacheability metadata bubbling API.
+   * To maintain backwards compatibility, we continue to support this, but
+   * support for this will be dropped in Drupal 9.0.0. This is especially useful
+   * when interacting with APIs that implicitly invoke rendering (for example:
+   * generating URLs): this allows those to "leak", and we collect their bubbled
+   * cacheability metadata automatically in a render context.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    *   The request object.
@@ -150,14 +158,25 @@ protected function renderResponseBody(Request $request, ResourceResponseInterfac
 
     // If there is data to send, serialize and set it as the response body.
     if ($data !== NULL) {
+      $serialization_context = [
+        'request' => $request,
+        'cacheability' => new CacheableMetadata(),
+      ];
+
+      // @deprecated In Drupal 8.4.0, will be removed before Drupal 9.0.0. Use
+      // explicit cacheability metadata bubbling instead. (The wrapping call to
+      // executeInRenderContext() will be removed before Drupal 9.0.0.)
       $context = new RenderContext();
       $output = $this->renderer
-        ->executeInRenderContext($context, function () use ($serializer, $data, $format) {
-          return $serializer->serialize($data, $format);
+        ->executeInRenderContext($context, function() use ($serializer, $data, $format, $serialization_context) {
+          return $serializer->serialize($data, $format, $serialization_context);
         });
-
-      if ($response instanceof CacheableResponseInterface && !$context->isEmpty()) {
-        $response->addCacheableDependency($context->pop());
+      if ($response instanceof CacheableResponseInterface) {
+        if (!$context->isEmpty()) {
+          @trigger_error('Implicit cacheability metadata bubbling (onto the global render context) in normalizers is deprecated since Drupal 8.4.0 and will be removed in Drupal 9.0.0. Use the "cacheability" serialization context instead, for explicit cacheability metadata bubbling.', E_USER_DEPRECATED);
+          $response->addCacheableDependency($context->pop());
+        }
+        $response->addCacheableDependency($serialization_context['cacheability']);
       }
 
       $response->setContent($output);
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
new file mode 100644
index 0000000000..9e67767517
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace Drupal\Tests\rest\Functional\EntityResource\EntityTest;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
+
+/**
+ * Test that exposed properties are actually exposed in REST.
+ *
+ * @group rest
+ */
+class EntityTestExposedPropertyNormalizerTest extends EntityTestResourceTestBase {
+
+  use AnonResourceTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'application/json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedNormalizedEntity() {
+    $expected = parent::getExpectedNormalizedEntity();
+    // The 'non_exposed_value' key will not be returned because setExposed(TRUE)
+    // was not called for this property.
+    // @see \Drupal\entity_test\Plugin\Field\FieldType\ExposedPropertyTestFieldItem::propertyDefinitions
+    $expected['field_test_exposed'] = [
+      [
+        'value' => 'value to expose',
+        'exposed_value' => 'Computed! value to expose',
+      ],
+    ];
+    return $expected;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createEntity() {
+    if (!FieldStorageConfig::loadByName('entity_test', 'field_test_exposed')) {
+      // Auto-create a field for testing.
+      FieldStorageConfig::create([
+        'entity_type' => 'entity_test',
+        'field_name' => 'field_test_exposed',
+        'type' => 'exposed_string_test',
+        'cardinality' => 1,
+        'translatable' => FALSE,
+      ])->save();
+      FieldConfig::create([
+        'entity_type' => 'entity_test',
+        'field_name' => 'field_test_exposed',
+        'bundle' => 'entity_test',
+        'label' => 'Test field with exposed and non-exposed properties',
+      ])->save();
+    }
+
+    $entity = parent::createEntity();
+    $entity->field_test_exposed = [
+      'value' => 'value to expose',
+    ];
+    $entity->save();
+    return $entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getNormalizedPostEntity() {
+    $post_entity = parent::getNormalizedPostEntity();
+    $post_entity['field_test_exposed'] = [
+      [
+        'value' => 'value to expose',
+      ],
+    ];
+    return $post_entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedCacheContexts() {
+    return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['request_format']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedCacheTags() {
+    return Cache::mergeTags(parent::getExpectedCacheTags(), ['you_are_it', 'no_tag_backs']);
+  }
+
+}
diff --git a/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
index 33beb373a1..d27b23e48a 100644
--- a/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\serialization\Normalizer;
 
+use Drupal\Core\TypedData\ComplexDataInterface;
+
 /**
  * Converts the Drupal entity object structures to a normalized array.
  *
@@ -14,6 +16,7 @@
  */
 class ComplexDataNormalizer extends NormalizerBase {
 
+  use ComplexDataPropertiesNormalizerTrait;
   /**
    * The interface or class that this Normalizer supports.
    *
@@ -25,10 +28,19 @@ class ComplexDataNormalizer extends NormalizerBase {
    * {@inheritdoc}
    */
   public function normalize($object, $format = NULL, array $context = []) {
-    $attributes = [];
-    /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
-    foreach ($object as $name => $property) {
-      $attributes[$name] = $this->serializer->normalize($property, $format, $context);
+    // $object will not always match $supportedInterfaceOrClass.
+    // @see \Drupal\serialization\Normalizer\EntityNormalizer
+    // Other normalizer that extend this class may only provide $object that
+    // implements \Traversable.
+    if ($object instanceof ComplexDataInterface) {
+      $attributes = $this->normalizeProperties($object, $format, $context);
+    }
+    else {
+      $attributes = [];
+      /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
+      foreach ($object as $name => $property) {
+        $attributes[$name] = $this->serializer->normalize($property, $format, $context);
+      }
     }
     return $attributes;
   }
diff --git a/core/modules/serialization/src/Normalizer/ComplexDataPropertiesNormalizerTrait.php b/core/modules/serialization/src/Normalizer/ComplexDataPropertiesNormalizerTrait.php
new file mode 100644
index 0000000000..8697d1ff29
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/ComplexDataPropertiesNormalizerTrait.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\TypedDataHelper;
+
+/**
+ * Normalization methods for complex data properties.
+ */
+trait ComplexDataPropertiesNormalizerTrait {
+
+  /**
+   * Normalizes complex data properties.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $data
+   *   The complex data being normalized.
+   * @param string $format
+   *   The normalization format.
+   * @param array $context
+   *   The context passed into the Normalizer.
+   *
+   * @return array
+   *   The normalized complex data properties.
+   */
+  protected function normalizeProperties(ComplexDataInterface $data, $format, array $context) {
+    $attributes = [];
+    foreach (TypedDataHelper::getExposedProperties($data) as $name => $property) {
+      $attribute = $this->serializer->normalize($property, $format, $context);
+      if ($attribute instanceof CacheableDependencyInterface && isset($context['cacheability'])) {
+        $context['cacheability']->addCacheableDependency($attribute);
+      }
+      if (is_object($attribute) && method_exists($attribute, '__toString')) {
+        $attribute = (string) $attribute;
+      }
+      $attributes[$name] = $attribute;
+    }
+    return $attributes;
+  }
+
+}
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php
index dc14d6f428..7e08b5b6aa 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php
@@ -7,8 +7,11 @@
 
 namespace Drupal\Tests\serialization\Unit\Normalizer;
 
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
 use Drupal\Core\TypedData\ComplexDataInterface;
-use Drupal\Core\TypedData\TraversableTypedDataInterface;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\serialization\Normalizer\ComplexDataNormalizer;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\Serializer\Serializer;
@@ -19,6 +22,8 @@
  */
 class ComplexDataNormalizerTest extends UnitTestCase {
 
+  use ExposedTypedDataTestTrait;
+
   /**
    * Test format string.
    *
@@ -44,103 +49,102 @@ protected function setUp() {
    * @covers ::supportsNormalization
    */
   public function testSupportsNormalization() {
-    $this->assertTrue($this->normalizer->supportsNormalization(new TestComplexData()));
+    $complexData = $this->prophesize(ComplexDataInterface::class)->reveal();
+    $this->assertTrue($this->normalizer->supportsNormalization($complexData));
     // Also test that an object not implementing ComplexDataInterface fails.
     $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
   }
 
   /**
+   * Test normalizing complex data.
+   *
    * @covers ::normalize
    */
-  public function testNormalize() {
-    $context = ['test' => 'test'];
-
+  public function testNormalizeComplexData() {
     $serializer_prophecy = $this->prophesize(Serializer::class);
 
-    $serializer_prophecy->normalize('A', static::TEST_FORMAT, $context)
-      ->shouldBeCalled();
-    $serializer_prophecy->normalize('B', static::TEST_FORMAT, $context)
+    $cacheable = $this->prophesize(TestCacheableDependencyInterface::class);
+    $cacheable->__toString()
+      ->willReturn('prop-as-string')
       ->shouldBeCalled();
+    $cacheable = $cacheable->reveal();
 
-    $this->normalizer->setSerializer($serializer_prophecy->reveal());
-
-    $complex_data = new TestComplexData(['a' => 'A', 'b' => 'B']);
-    $this->normalizer->normalize($complex_data, static::TEST_FORMAT, $context);
-
-  }
-
-}
-
-/**
- * Test class implementing ComplexDataInterface and IteratorAggregate.
- */
-class TestComplexData implements \IteratorAggregate, ComplexDataInterface {
-
-  private $values;
-
-  public function __construct(array $values = []) {
-    $this->values = $values;
-  }
-
-  public function getIterator() {
-    return new \ArrayIterator($this->values);
-  }
-
-  public function applyDefaultValue($notify = TRUE) {
-  }
-
-  public static function createInstance($definition, $name = NULL, TraversableTypedDataInterface $parent = NULL) {
-  }
+    $exposed_cacheable_property = $this->getTypedDataProperty();
+    $exposed_noncacheable_property = $this->getTypedDataProperty();
 
-  public function get($property_name) {
-  }
 
-  public function getConstraints() {
-  }
-
-  public function getDataDefinition() {
-  }
-
-  public function getName() {
-  }
-
-  public function getParent() {
-  }
+    $cacheableMetaData = $this->prophesize(RefinableCacheableDependencyInterface::class);
+    $cacheableMetaData->addCacheableDependency($cacheable)
+      ->shouldBeCalled();
+    $serialization_context = [
+      'cacheability' => $cacheableMetaData->reveal(),
+    ];
 
-  public function getProperties($include_computed = FALSE) {
-  }
+    $serializer_prophecy->normalize($exposed_noncacheable_property, static::TEST_FORMAT, $serialization_context)
+      ->willReturn('A-normalized')
+      ->shouldBeCalled();
+    $serializer_prophecy->normalize($exposed_cacheable_property, static::TEST_FORMAT, $serialization_context)
+      ->willReturn($cacheable)
+      ->shouldBeCalled();
 
-  public function getPropertyPath() {
-  }
+    $this->normalizer->setSerializer($serializer_prophecy->reveal());
 
-  public function getRoot() {
-  }
+    $complex_data = $this->prophesize(ComplexDataInterface::class);
+    $complex_data->getProperties(TRUE)
+      ->willReturn([
+        'prop:a' => $exposed_noncacheable_property,
+        'prop:cacheable' => $exposed_cacheable_property,
+        'prop:nonexposed' => $this->getTypedDataProperty(FALSE),
+      ])
+      ->shouldBeCalled();
 
-  public function getString() {
+    $normalized = $this->normalizer->normalize($complex_data->reveal(), static::TEST_FORMAT, $serialization_context);
+    $this->assertEquals(['prop:a' => 'A-normalized', 'prop:cacheable' => 'prop-as-string'], $normalized);
   }
 
-  public function getValue() {
-  }
+  /**
+   * Test normalize() where $object does not implement ComplexDataInterface.
+   *
+   * Normalizers extending ComplexDataNormalizer may have a different supported
+   * class.
+   *
+   * @covers ::normalize
+   */
+  public function testNormalizeNonComplex() {
+    $normalizer = new TestExtendedNormalizer();
+    $serialization_context = ['test' => 'test'];
 
-  public function isEmpty() {
-  }
+    $serializer_prophecy = $this->prophesize(Serializer::class);
+    $serializer_prophecy->normalize('A', static::TEST_FORMAT, $serialization_context)
+      ->willReturn('A-normalized')
+      ->shouldBeCalled();
+    $serializer_prophecy->normalize('B', static::TEST_FORMAT, $serialization_context)
+      ->willReturn('B-normalized')
+      ->shouldBeCalled();
 
-  public function onChange($name) {
-  }
+    $normalizer->setSerializer($serializer_prophecy->reveal());
 
-  public function set($property_name, $value, $notify = TRUE) {
-  }
+    $stdClass = new \stdClass();
+    $stdClass->a = 'A';
+    $stdClass->b = 'B';
 
-  public function setContext($name = NULL, TraversableTypedDataInterface $parent = NULL) {
-  }
+    $normalized = $normalizer->normalize($stdClass, static::TEST_FORMAT, $serialization_context);
+    $this->assertEquals(['a' => 'A-normalized', 'b' => 'B-normalized'], $normalized);
 
-  public function setValue($value, $notify = TRUE) {
   }
 
-  public function toArray() {
-  }
+}
 
-  public function validate() {
-  }
+/**
+ * Test interface used for mocking.
+ */
+interface TestCacheableDependencyInterface extends CacheableDependencyInterface {
+  public function __toString();
+}
 
+/**
+ * Test normalizer with a different supported class.
+ */
+class TestExtendedNormalizer extends ComplexDataNormalizer {
+  protected $supportedInterfaceOrClass = \stdClass::class;
 }
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
index e0561a1003..9468901eed 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
@@ -4,6 +4,9 @@
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\Plugin\DataType\StringData;
+use Drupal\Core\TypedData\Type\StringInterface;
 use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
@@ -23,6 +26,8 @@
  */
 class EntityReferenceFieldItemNormalizerTest extends UnitTestCase {
 
+  use ExposedTypedDataTestTrait;
+
   /**
    * The mock serializer.
    *
@@ -122,6 +127,10 @@ public function testNormalize() {
       ->willReturn($entity_reference)
       ->shouldBeCalled();
 
+    $this->fieldItem->getProperties(TRUE)
+      ->willReturn(['target_id' => $this->getTypedDataProperty()])
+      ->shouldBeCalled();
+
     $normalized = $this->normalizer->normalize($this->fieldItem->reveal());
 
     $expected = [
@@ -146,6 +155,10 @@ public function testNormalizeWithNoEntity() {
       ->willReturn($entity_reference->reveal())
       ->shouldBeCalled();
 
+    $this->fieldItem->getProperties(TRUE)
+      ->willReturn(['target_id' => $this->getTypedDataProperty()])
+      ->shouldBeCalled();
+
     $normalized = $this->normalizer->normalize($this->fieldItem->reveal());
 
     $expected = [
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/ExposedTypedDataTestTrait.php b/core/modules/serialization/tests/src/Unit/Normalizer/ExposedTypedDataTestTrait.php
new file mode 100644
index 0000000000..e16b60f98c
--- /dev/null
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/ExposedTypedDataTestTrait.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Tests\serialization\Unit\Normalizer;
+
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+
+/**
+ * Test trait that provides functions for retrieving typed data properties.
+ */
+trait ExposedTypedDataTestTrait {
+
+  /**
+   * Gets a typed data property.
+   *
+   * @param bool $exposed
+   *   Whether the typed data property should be exposed.
+   *
+   * @return \Drupal\Core\TypedData\TypedDataInterface
+   *   The typed data property.
+   */
+  protected function getTypedDataProperty($exposed = TRUE) {
+    $exposed_definition = $this->prophesize(DataDefinitionInterface::class);
+    $exposed_definition->isExposed()
+      ->willReturn($exposed)
+      ->shouldBeCalled();
+    $exposed_definition = $exposed_definition->reveal();
+
+    $exposed_property = $this->prophesize(TypedDataInterface::class);
+    $exposed_property->getDataDefinition()
+      ->willReturn($exposed_definition)
+      ->shouldBeCalled();
+    return $exposed_property->reveal();
+  }
+
+}
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
index fd1fc9ce9f..f671aabb18 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
@@ -18,6 +18,8 @@
  */
 class TimestampItemNormalizerTest extends UnitTestCase {
 
+  use ExposedTypedDataTestTrait;
+
   /**
    * @var \Drupal\serialization\Normalizer\TimestampItemNormalizer
    */
@@ -77,8 +79,18 @@ public function testNormalize() {
     $timestamp_item->getIterator()
       ->willReturn(new \ArrayIterator(['value' => 1478422920]));
 
-    $serializer = new Serializer();
-    $this->normalizer->setSerializer($serializer);
+    $exposed_property = $this->getTypedDataProperty();
+    $timestamp_item->getProperties(TRUE)
+      ->willReturn(['value' => $exposed_property])
+      ->shouldBeCalled();
+
+    $serializer_prophecy = $this->prophesize(Serializer::class);
+
+    $serializer_prophecy->normalize($exposed_property, NULL, [])
+      ->willReturn(1478422920)
+      ->shouldBeCalled();
+
+    $this->normalizer->setSerializer($serializer_prophecy->reveal());
 
     $normalized = $this->normalizer->normalize($timestamp_item->reveal());
     $this->assertSame($expected, $normalized);
diff --git a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.data_types.schema.yml b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.data_types.schema.yml
new file mode 100644
index 0000000000..bda7a93205
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.data_types.schema.yml
@@ -0,0 +1,15 @@
+# Schema for the configuration of the exposed string field type.
+
+field.storage_settings.exposed_string_test:
+  type: mapping
+  label: 'String settings'
+  mapping:
+    max_length:
+      type: integer
+      label: 'Maximum length'
+    case_sensitive:
+      type: boolean
+      label: 'Case sensitive'
+    is_ascii:
+      type: boolean
+      label: 'Contains US ASCII characters only'
diff --git a/core/modules/system/tests/modules/entity_test/src/ComputedString.php b/core/modules/system/tests/modules/entity_test/src/ComputedString.php
new file mode 100644
index 0000000000..18a3a0c724
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/ComputedString.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\entity_test;
+
+use Drupal\Core\Render\BubbleableMetadata;
+
+/**
+ * Computed string for testing.
+ *
+ * @see \Drupal\entity_test\Plugin\DataType\ComputedStringData
+ */
+class ComputedString extends BubbleableMetadata {
+
+  /**
+   * The string value before computation.
+   *
+   * @var string
+   */
+  protected $notComputedValue;
+
+  /**
+   * Constructs a ComputedString object.
+   *
+   * @param string $not_computed_value
+   *   The text value before being computed.
+   */
+  public function __construct($not_computed_value) {
+    $this->notComputedValue = $not_computed_value;
+    $this->cacheContexts = ['request_format'];
+    $this->cacheTags = ['you_are_it', 'no_tag_backs'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __toString() {
+    // Computation is simple concatenation for test.
+    return "Computed! " . $this->notComputedValue;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/DataType/ComputedStringData.php b/core/modules/system/tests/modules/entity_test/src/Plugin/DataType/ComputedStringData.php
new file mode 100644
index 0000000000..888b31c6f6
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Plugin/DataType/ComputedStringData.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\entity_test\Plugin\DataType;
+
+use Drupal\Core\TypedData\Plugin\DataType\StringData;
+use Drupal\entity_test\ComputedString;
+
+/**
+ * The exposed string test data type.
+ *
+ * This simply concatenates 'Exposed! ' before the 'value' property.
+ *
+ * @DataType(
+ *   id = "computed_string",
+ *   label = @Translation("Exposed String")
+ * )
+ */
+class ComputedStringData extends StringData {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    /** @var \Drupal\Core\Field\FieldItemInterface $item */
+    $item = $this->getParent();
+    $computed = new ComputedString($item->get('value')->getString());
+    return $computed;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCastedValue() {
+    return $this->getValue();
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ExposedPropertyTestFieldItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ExposedPropertyTestFieldItem.php
new file mode 100644
index 0000000000..35b37f128f
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/ExposedPropertyTestFieldItem.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\entity_test\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;
+use Drupal\entity_test\Plugin\DataType\ComputedStringData;
+
+/**
+ * Defines the 'Exposed Property' entity test field type.
+ *
+ * @FieldType(
+ *   id = "exposed_string_test",
+ *   label = @Translation("Exposed Property (test)"),
+ *   description = @Translation("A field containing two computed string values, one exposed and one not exposed."),
+ *   category = @Translation("Test"),
+ *   default_widget = "string_textfield",
+ *   default_formatter = "string"
+ * )
+ */
+class ExposedPropertyTestFieldItem extends StringItem {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties = parent::propertyDefinitions($field_definition);
+
+    // Add a computed property that is exposed.
+    $properties['exposed_value'] = DataDefinition::create('computed_string')
+      ->setLabel(new TranslatableMarkup('Text value exposed'))
+      ->setComputed(TRUE)
+      ->setClass(ComputedStringData::class)
+      ->setExposed(TRUE);
+    // Add a computed property that is NOT exposed.
+    $properties['non_exposed_value'] = DataDefinition::create('computed_string')
+      ->setLabel(new TranslatableMarkup('Text value non-exposed'))
+      ->setComputed(TRUE)
+      ->setClass(ComputedStringData::class);
+    return $properties;
+  }
+
+}
