diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
index 991385f78b..b13a1640fc 100644
--- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
+++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
@@ -855,4 +855,12 @@ public function __clone() {
     $this->propertyDefinitions = NULL;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isInternal() {
+    // All fields are not internal unless explicitly set.
+    return !empty($this->definition['internal']);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Field/FieldConfigBase.php b/core/lib/Drupal/Core/Field/FieldConfigBase.php
index e53e84b6c8..a32ff423be 100644
--- a/core/lib/Drupal/Core/Field/FieldConfigBase.php
+++ b/core/lib/Drupal/Core/Field/FieldConfigBase.php
@@ -591,4 +591,15 @@ public function addPropertyConstraints($name, array $constraints) {
     return $this;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isInternal() {
+    // Respect the definition, otherwise default to TRUE for computed fields.
+    if (isset($this->definition['internal'])) {
+      return $this->definition['internal'];
+    }
+    return $this->isComputed();
+  }
+
 }
diff --git a/core/lib/Drupal/Core/TypedData/DataDefinition.php b/core/lib/Drupal/Core/TypedData/DataDefinition.php
index 52a4394cd7..332659feaa 100644
--- a/core/lib/Drupal/Core/TypedData/DataDefinition.php
+++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php
@@ -352,4 +352,28 @@ public function __sleep() {
     return array_keys($vars);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function isInternal() {
+    // Respect the definition, otherwise default to TRUE for computed fields.
+    if (isset($this->definition['internal'])) {
+      return $this->definition['internal'];
+    }
+    return $this->isComputed();
+  }
+
+  /**
+   * Sets the whether the data value should be internal.
+   *
+   * @param bool $internal
+   *   Whether the data value should be internal.
+   *
+   * @return $this
+   */
+  public function setInternal($internal) {
+    $this->definition['internal'] = $internal;
+    return $this;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
index 64d779c317..2a8e89255f 100644
--- a/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
+++ b/core/lib/Drupal/Core/TypedData/DataDefinitionInterface.php
@@ -218,4 +218,16 @@ public function getConstraint($constraint_name);
    */
   public function addConstraint($constraint_name, $options = NULL);
 
+  /**
+   * Determines whether the data value is internal.
+   *
+   * Internal properties will not be returned during the normalization procces.
+   * By default, calculated properties will be internal unless otherwise
+   * specified.
+   *
+   * @return bool
+   *   Whether the data value is internal.
+   */
+  public function isInternal();
+
 }
diff --git a/core/lib/Drupal/Core/TypedData/TypedDataInternalPropertiesHelper.php b/core/lib/Drupal/Core/TypedData/TypedDataInternalPropertiesHelper.php
new file mode 100644
index 0000000000..96a240ce81
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/TypedDataInternalPropertiesHelper.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\Core\TypedData;
+
+/**
+ * Helper class for internal properties.
+ */
+class TypedDataInternalPropertiesHelper {
+
+  /**
+   * Gets an array non-internal properties from a complex data object.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $data
+   *   The complex data object.
+   * @param array $context
+   *   The context passed into the Normalizer.
+   *
+   * @return \Drupal\Core\TypedData\TypedDataInterface[]
+   *   The non-internal properties, keyed by property name.
+   */
+  public static function getNonInternalProperties(ComplexDataInterface $data, array $context) {
+    return array_filter($data->getProperties(TRUE), function (TypedDataInterface $property) use ($context) {
+      return !$property->getDataDefinition()->isInternal();
+    });
+  }
+
+}
diff --git a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
index eb45247285..22d59aa6cc 100644
--- a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
+++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
@@ -6,6 +6,7 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
 use Drupal\hal\LinkManager\LinkManagerInterface;
 use Drupal\serialization\Normalizer\FieldableEntityNormalizerTrait;
 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
@@ -72,15 +73,10 @@ public function normalize($entity, $format = NULL, array $context = []) {
       ],
     ];
 
+    $field_items = TypedDataInternalPropertiesHelper::getNonInternalProperties($entity->getTypedData(), $context);
     // If the fields to use were specified, only output those field values.
     if (isset($context['included_fields'])) {
-      $field_items = [];
-      foreach ($context['included_fields'] as $field_name) {
-        $field_items[] = $entity->get($field_name);
-      }
-    }
-    else {
-      $field_items = $entity->getFields();
+      $field_items = array_intersect_key($field_items, array_flip($context['included_fields']));
     }
     foreach ($field_items as $field) {
       // Continue if the current user does not have access to view this field.
diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
index 4c4ea51f29..12db438169 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\Core\TypedData\TypedDataInternalPropertiesHelper;
 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
 
 /**
@@ -91,7 +92,7 @@ protected function normalizedFieldValues(FieldItemInterface $field_item, $format
     // 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) {
+    foreach (TypedDataInternalPropertiesHelper::getNonInternalProperties($field_item, $context) as $property_name => $property) {
       $normalized[$property_name] = $this->serializer->normalize($property, $format, $context);
     }
 
diff --git a/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonInternalPropertyNormalizerTest.php b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonInternalPropertyNormalizerTest.php
new file mode 100644
index 0000000000..ce02ca6b13
--- /dev/null
+++ b/core/modules/hal/tests/src/Functional/EntityResource/EntityTest/EntityTestHalJsonInternalPropertyNormalizerTest.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Drupal\Tests\hal\Functional\EntityResource\EntityTest;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait;
+use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
+
+/**
+ * Test that internal properties are not exposed in the 'hal_json' format.
+ *
+ * @group hal
+ */
+class EntityTestHalJsonInternalPropertyNormalizerTest extends EntityTestHalJsonAnonTest {
+
+  use AnonResourceTestTrait;
+  use HalEntityNormalizationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['hal'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedNormalizedEntity() {
+    $default_normalization = parent::getExpectedNormalizedEntity();
+
+    $normalization = $this->applyHalFieldNormalization($default_normalization);
+    // The 'internal_value' property in test field type will not be returned in
+    // normalization because setInternal(FALSE) was not called for this
+    // property.
+    // @see \Drupal\entity_test\Plugin\Field\FieldType\InternalPropertyTestFieldItem::propertyDefinitions
+    $normalization['field_test_internal'] = [
+      [
+        'value' => 'value to be internal',
+        'non_internal_value' => 'Computed! value to be internal',
+      ],
+    ];
+    return $normalization;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createEntity() {
+    if (!FieldStorageConfig::loadByName('entity_test', 'field_test_internal')) {
+      FieldStorageConfig::create([
+        'entity_type' => 'entity_test',
+        'field_name' => 'field_test_internal',
+        'type' => 'internal_property_test',
+        'cardinality' => 1,
+        'translatable' => FALSE,
+      ])->save();
+      FieldConfig::create([
+        'entity_type' => 'entity_test',
+        'field_name' => 'field_test_internal',
+        'bundle' => 'entity_test',
+        'label' => 'Test field with internal and non-internal properties',
+      ])->save();
+    }
+
+    $entity = parent::createEntity();
+    $entity->field_test_internal = [
+      'value' => 'value to be internal',
+    ];
+    $entity->save();
+    return $entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getNormalizedPostEntity() {
+    return parent::getNormalizedPostEntity() + [
+      'field_test_internal' => [
+        [
+          'value' => 'value to be internal',
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonInternalPropertyNormalizerTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonInternalPropertyNormalizerTest.php
new file mode 100644
index 0000000000..7c64e3fdac
--- /dev/null
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestJsonInternalPropertyNormalizerTest.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 internal properties are not exposed in the 'json' format.
+ *
+ * @group rest
+ */
+class EntityTestJsonInternalPropertyNormalizerTest extends EntityTestResourceTestBase {
+
+  use AnonResourceTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'application/json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedNormalizedEntity() {
+    $expected = parent::getExpectedNormalizedEntity();
+    // The 'internal_value' property in test field type is not exposed in the
+    // normalization because setInternal(FALSE) was not called for this
+    // property.
+    // @see \Drupal\entity_test\Plugin\Field\FieldType\InternalPropertyTestFieldItem::propertyDefinitions
+    $expected['field_test_internal'] = [
+      [
+        'value' => 'value to be internal',
+        'non_internal_value' => 'Computed! value to be internal',
+      ],
+    ];
+    return $expected;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createEntity() {
+    if (!FieldStorageConfig::loadByName('entity_test', 'field_test_internal')) {
+      FieldStorageConfig::create([
+        'entity_type' => 'entity_test',
+        'field_name' => 'field_test_internal',
+        'type' => 'internal_property_test',
+        'cardinality' => 1,
+        'translatable' => FALSE,
+      ])->save();
+      FieldConfig::create([
+        'entity_type' => 'entity_test',
+        'field_name' => 'field_test_internal',
+        'bundle' => 'entity_test',
+        'label' => 'Test field with internal and non-internal properties',
+      ])->save();
+    }
+
+    $entity = parent::createEntity();
+    $entity->field_test_internal = [
+      'value' => 'value to be internal',
+    ];
+    $entity->save();
+    return $entity;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getNormalizedPostEntity() {
+    return parent::getNormalizedPostEntity() + [
+      'field_test_internal' => [
+        [
+          'value' => 'value to be internal',
+        ],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
index e2c0ccdf65..5ca1f7e44c 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
@@ -53,9 +53,20 @@ protected function setUpAuthorization($method) {
    * {@inheritdoc}
    */
   protected function createEntity() {
+    // Set flag so that internal field 'internal_string_field' is created.
+    // @see \entity_test_entity_base_field_info()
+    $this->container->get('state')->set('entity_test.internal_field', TRUE);
+    \Drupal::entityDefinitionUpdateManager()->applyUpdates();
+
     $entity_test = EntityTest::create([
       'name' => 'Llama',
       'type' => 'entity_test',
+      // Set a value for the internal field to confirm that it will not be
+      // returned in normalization.
+      // @see \entity_test_entity_base_field_info().
+      'internal_string_field' => [
+        'value' => 'value to be internal',
+      ],
     ]);
     $entity_test->setOwnerId(0);
     $entity_test->save();
diff --git a/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
index 33beb373a1..3478124b40 100644
--- a/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\serialization\Normalizer;
 
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
+
 /**
  * Converts the Drupal entity object structures to a normalized array.
  *
@@ -26,6 +29,13 @@ class ComplexDataNormalizer extends NormalizerBase {
    */
   public function normalize($object, $format = NULL, array $context = []) {
     $attributes = [];
+    // $object will not always match $supportedInterfaceOrClass.
+    // @see \Drupal\serialization\Normalizer\EntityNormalizer
+    // Other normalizers that extend this class may only provide $object that
+    // implements \Traversable.
+    if ($object instanceof ComplexDataInterface) {
+      $object = TypedDataInternalPropertiesHelper::getNonInternalProperties($object, $context);
+    }
     /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
     foreach ($object as $name => $property) {
       $attributes[$name] = $this->serializer->normalize($property, $format, $context);
diff --git a/core/modules/serialization/src/Normalizer/ContentEntityNormalizer.php b/core/modules/serialization/src/Normalizer/ContentEntityNormalizer.php
index d3abef6f12..6ac8927df6 100644
--- a/core/modules/serialization/src/Normalizer/ContentEntityNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/ContentEntityNormalizer.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\serialization\Normalizer;
 
+use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
+
 /**
  * Normalizes/denormalizes Drupal content entities into an array structure.
  */
@@ -21,7 +23,8 @@ public function normalize($entity, $format = NULL, array $context = []) {
     ];
 
     $attributes = [];
-    foreach ($entity as $name => $field_items) {
+    /** @var \Drupal\Core\Entity\Entity $entity */
+    foreach (TypedDataInternalPropertiesHelper::getNonInternalProperties($entity->getTypedData(), $context) as $name => $field_items) {
       if ($field_items->access('view', $context['account'])) {
         $attributes[$name] = $this->serializer->normalize($field_items, $format, $context);
       }
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/ComplexDataNormalizerTest.php
index dc14d6f428..1bf6cf4a81 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 InternalTypedDataTestTrait;
+
   /**
    * Test format string.
    *
@@ -44,103 +45,77 @@ protected function setUp() {
    * @covers ::supportsNormalization
    */
   public function testSupportsNormalization() {
-    $this->assertTrue($this->normalizer->supportsNormalization(new TestComplexData()));
+    $complex_data = $this->prophesize(ComplexDataInterface::class)->reveal();
+    $this->assertTrue($this->normalizer->supportsNormalization($complex_data));
     // 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)
+    $non_internal_property = $this->getTypedDataProperty(FALSE);
+
+    $serializer_prophecy->normalize($non_internal_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' => $non_internal_property,
+        'prop:internal' => $this->getTypedDataProperty(TRUE),
+      ])
+      ->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/ContentEntityNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/ContentEntityNormalizerTest.php
index 4beecf420d..09c97f80a7 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/ContentEntityNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/ContentEntityNormalizerTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\serialization\Unit\Normalizer;
 
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\DataDefinitionInterface;
 use Drupal\serialization\Normalizer\ContentEntityNormalizer;
 use Drupal\Tests\UnitTestCase;
 
@@ -101,6 +103,7 @@ public function testNormalizeWithAccountContext() {
     $definitions = [
       'field_1' => $this->createMockFieldListItem(TRUE, $mock_account),
       'field_2' => $this->createMockFieldListItem(FALSE, $mock_account),
+      // @todo Add test internal field.
     ];
     $content_entity_mock = $this->createMockForContentEntity($definitions);
 
@@ -121,11 +124,15 @@ public function testNormalizeWithAccountContext() {
   public function createMockForContentEntity($definitions) {
     $content_entity_mock = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase')
       ->disableOriginalConstructor()
-      ->setMethods(['getFields'])
+      ->setMethods(['getTypedData'])
       ->getMockForAbstractClass();
-    $content_entity_mock->expects($this->once())
-      ->method('getFields')
-      ->will($this->returnValue($definitions));
+    $typed_data = $this->prophesize(ComplexDataInterface::class);
+    $typed_data->getProperties(TRUE)
+      ->willReturn($definitions)
+      ->shouldBeCalled();
+    $content_entity_mock->expects($this->any())
+      ->method('getTypedData')
+      ->will($this->returnValue($typed_data->reveal()));
 
     return $content_entity_mock;
   }
@@ -143,7 +150,13 @@ protected function createMockFieldListItem($access = TRUE, $user_context = NULL)
       ->method('access')
       ->with('view', $user_context)
       ->will($this->returnValue($access));
-
+    $data_definition = $this->prophesize(DataDefinitionInterface::class);
+    $data_definition->isInternal()
+      ->willReturn(FALSE)
+      ->shouldBeCalled();
+    $mock->expects($this->once())
+      ->method('getDataDefinition')
+      ->will($this->returnValue($data_definition->reveal()));
     return $mock;
   }
 
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
index e0561a1003..de2a4a6542 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 InternalTypedDataTestTrait;
+
   /**
    * The mock serializer.
    *
@@ -122,6 +124,10 @@ public function testNormalize() {
       ->willReturn($entity_reference)
       ->shouldBeCalled();
 
+    $this->fieldItem->getProperties(TRUE)
+      ->willReturn(['target_id' => $this->getTypedDataProperty(FALSE)])
+      ->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(FALSE)])
+      ->shouldBeCalled();
+
     $normalized = $this->normalizer->normalize($this->fieldItem->reveal());
 
     $expected = [
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/InternalTypedDataTestTrait.php b/core/modules/serialization/tests/src/Unit/Normalizer/InternalTypedDataTestTrait.php
new file mode 100644
index 0000000000..3829934b50
--- /dev/null
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/InternalTypedDataTestTrait.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Tests\serialization\Unit\Normalizer;
+
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+
+/**
+ * Trait that provides mocked typed data objects.
+ */
+trait InternalTypedDataTestTrait {
+
+  /**
+   * Gets a typed data property.
+   *
+   * @param bool $internal
+   *   Whether the typed data property is internal.
+   *
+   * @return \Drupal\Core\TypedData\TypedDataInterface
+   *   The typed data property.
+   */
+  protected function getTypedDataProperty($internal = TRUE) {
+    $definition = $this->prophesize(DataDefinitionInterface::class);
+    $definition->isInternal()
+      ->willReturn($internal)
+      ->shouldBeCalled();
+    $definition = $definition->reveal();
+
+    $property = $this->prophesize(TypedDataInterface::class);
+    $property->getDataDefinition()
+      ->willReturn($definition)
+      ->shouldBeCalled();
+    return $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..c4e351424a 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 InternalTypedDataTestTrait;
+
   /**
    * @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);
+    $value_property = $this->getTypedDataProperty(FALSE);
+    $timestamp_item->getProperties(TRUE)
+      ->willReturn(['value' => $value_property])
+      ->shouldBeCalled();
+
+    $serializer_prophecy = $this->prophesize(Serializer::class);
+
+    $serializer_prophecy->normalize($value_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..cebaf03905
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.data_types.schema.yml
@@ -0,0 +1,5 @@
+# Schema for the configuration of the 'internal property test' field type.
+
+field.storage_settings.internal_property_test:
+  type: field.storage_settings.string
+  label: 'Internal property settings'
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index bcf55f48b2..025cb5fc2f 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -109,6 +109,11 @@ function entity_test_entity_type_alter(array &$entity_types) {
 function entity_test_entity_base_field_info(EntityTypeInterface $entity_type) {
   $fields = [];
 
+  if ($entity_type->id() === 'entity_test' && \Drupal::state()->get('entity_test.internal_field')) {
+    $fields['internal_string_field'] = BaseFieldDefinition::create('string')
+      ->setLabel('Internal field')
+      ->setInternal(TRUE);
+  }
   if ($entity_type->id() == 'entity_test_mulrev' && \Drupal::state()->get('entity_test.field_test_item')) {
     $fields['field_test_item'] = BaseFieldDefinition::create('field_test')
       ->setLabel(t('Field test'))
diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/InternalPropertyTestFieldItem.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/InternalPropertyTestFieldItem.php
new file mode 100644
index 0000000000..2bc8154cf9
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/FieldType/InternalPropertyTestFieldItem.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\TypedData\ComputedString;
+
+/**
+ * Defines the 'Internal Property' entity test field type.
+ *
+ * @FieldType(
+ *   id = "internal_property_test",
+ *   label = @Translation("Internal Property (test)"),
+ *   description = @Translation("A field containing one string, from which two strings are computed (one internal, one not)."),
+ *   category = @Translation("Test"),
+ *   default_widget = "string_textfield",
+ *   default_formatter = "string"
+ * )
+ */
+class InternalPropertyTestFieldItem extends StringItem {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties = parent::propertyDefinitions($field_definition);
+
+    // Add a computed property that is non-internal.
+    $properties['non_internal_value'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Computed string, non-internal property'))
+      ->setComputed(TRUE)
+      ->setClass(ComputedString::class)
+      ->setInternal(FALSE);
+    // Add a computed property that is internal.
+    $properties['internal_value'] = DataDefinition::create('string')
+      ->setLabel(new TranslatableMarkup('Computed string, internal property'))
+      ->setComputed(TRUE)
+      ->setClass(ComputedString::class);
+    return $properties;
+  }
+
+}
diff --git a/core/modules/system/tests/modules/entity_test/src/TypedData/ComputedString.php b/core/modules/system/tests/modules/entity_test/src/TypedData/ComputedString.php
new file mode 100644
index 0000000000..121699c807
--- /dev/null
+++ b/core/modules/system/tests/modules/entity_test/src/TypedData/ComputedString.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\entity_test\TypedData;
+
+use Drupal\Core\TypedData\TypedData;
+
+/**
+ * A computed property for test strings.
+ */
+class ComputedString extends TypedData {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    /** @var \Drupal\Core\Field\FieldItemInterface $item */
+    $item = $this->getParent();
+    $computed_value = "Computed! " . $item->get('value')->getString();
+
+    return $computed_value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCastedValue() {
+    return $this->getString();
+  }
+
+}
