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..60d8ca76f5
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionTrait.php
@@ -0,0 +1,36 @@
+<?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/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
new file mode 100644
index 0000000000..d1fc90b177
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestExposedPropertyNormalizerTest.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Drupal\Tests\rest\Functional\EntityResource\EntityTest;
+
+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' property in test field type will not return in
+    // normalization 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')) {
+      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() +
+      [
+        'field_test_exposed' =>
+          [
+            'value' => 'value to expose',
+          ],
+      ];
+    return $post_entity;
+  }
+
+}
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..d9d146ebad
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/ComplexDataPropertiesNormalizerTrait.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+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) {
+      $attributes[$name] = $this->serializer->normalize($property, $format, $context);
+    }
+    return $attributes;
+  }
+
+}
diff --git a/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php b/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php
index 60ce7d08f6..3e7be7cf5e 100644
--- a/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/TypedDataNormalizer.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\serialization\Normalizer;
 
+use Drupal\Core\TypedData\PrimitiveInterface;
+
 /**
  * Converts typed data objects to arrays.
  */
@@ -18,7 +20,11 @@ class TypedDataNormalizer extends NormalizerBase {
    * {@inheritdoc}
    */
   public function normalize($object, $format = NULL, array $context = []) {
-    return $object->getValue();
+    $value = $object->getValue();
+    if (is_object($value) && $object instanceof PrimitiveInterface) {
+      return $object->getCastedValue();
+    }
+    return $value;
   }
 
 }
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php
index dc14d6f428..a56a723a1f 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php
@@ -8,7 +8,6 @@
 namespace Drupal\Tests\serialization\Unit\Normalizer;
 
 use Drupal\Core\TypedData\ComplexDataInterface;
-use Drupal\Core\TypedData\TraversableTypedDataInterface;
 use Drupal\serialization\Normalizer\ComplexDataNormalizer;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Component\Serializer\Serializer;
@@ -19,6 +18,8 @@
  */
 class ComplexDataNormalizerTest extends UnitTestCase {
 
+  use ExposedTypedDataTestTrait;
+
   /**
    * Test format string.
    *
@@ -44,103 +45,77 @@ 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)
+    $exposed_property = $this->getTypedDataProperty();
+
+    $serializer_prophecy->normalize($exposed_property, static::TEST_FORMAT, [])
+      ->willReturn('A-normalized')
       ->shouldBeCalled();
 
     $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) {
-  }
-
-  public function get($property_name) {
-  }
-
-  public function getConstraints() {
-  }
-
-  public function getDataDefinition() {
-  }
-
-  public function getName() {
-  }
-
-  public function getParent() {
-  }
-
-  public function getProperties($include_computed = FALSE) {
-  }
-
-  public function getPropertyPath() {
-  }
-
-  public function getRoot() {
-  }
+    $complex_data = $this->prophesize(ComplexDataInterface::class);
+    $complex_data->getProperties(TRUE)
+      ->willReturn([
+        'prop:a' => $exposed_property,
+        'prop:nonexposed' => $this->getTypedDataProperty(FALSE),
+      ])
+      ->shouldBeCalled();
 
-  public function getString() {
+    $normalized = $this->normalizer->normalize($complex_data->reveal(), static::TEST_FORMAT);
+    $this->assertEquals(['prop:a' => 'A-normalized'], $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 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..0d2b7deaef 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
@@ -23,6 +23,8 @@
  */
 class EntityReferenceFieldItemNormalizerTest extends UnitTestCase {
 
+  use ExposedTypedDataTestTrait;
+
   /**
    * The mock serializer.
    *
@@ -122,6 +124,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 +152,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..5d14d79fa4
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/ComputedString.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\entity_test;
+
+/**
+ * Computed string for testing.
+ *
+ * @see \Drupal\entity_test\Plugin\DataType\ComputedStringData
+ */
+class ComputedString {
+
+  /**
+   * 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;
+  }
+
+  /**
+   * {@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..6e7462d932
--- /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 (string) $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;
+  }
+
+}
