diff --git a/core/lib/Drupal/Core/TypedData/Plugin/DataType/Timestamp.php b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Timestamp.php
index 8c72c859cee9c2d959ee2a0f3ddc374214055bfc..60cd9b423b6d64658c3f53baed36b4995ef6b865 100644
--- a/core/lib/Drupal/Core/TypedData/Plugin/DataType/Timestamp.php
+++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/Timestamp.php
@@ -26,7 +26,7 @@ class Timestamp extends IntegerData implements DateTimeInterface {
    * {@inheritdoc}
    */
   public function getDateTime() {
-    if ($this->value) {
+    if (isset($this->value)) {
       return DrupalDateTime::createFromTimestamp($this->value);
     }
   }
diff --git a/core/modules/datetime/tests/src/Functional/EntityResource/EntityTest/EntityTestDateonlyTest.php b/core/modules/datetime/tests/src/Functional/EntityResource/EntityTest/EntityTestDateonlyTest.php
index 0fb404d2b970987946107b11f85bb7fa4e7dc95b..e8c71ef0ccf3f217407cefc7d804e5c7a227bb60 100644
--- a/core/modules/datetime/tests/src/Functional/EntityResource/EntityTest/EntityTestDateonlyTest.php
+++ b/core/modules/datetime/tests/src/Functional/EntityResource/EntityTest/EntityTestDateonlyTest.php
@@ -136,7 +136,7 @@ protected function assertNormalizationEdgeCases($method, Url $url, array $reques
 
       $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
       $response = $this->request($method, $url, $request_options);
-      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The datetime value '{$value}' is invalid for the format 'Y-m-d'\n";
+      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\" (date-only).";
       $this->assertResourceErrorResponse(422, $message, $response);
 
       // DX: 422 when value is not a valid date.
@@ -146,7 +146,7 @@ protected function assertNormalizationEdgeCases($method, Url $url, array $reques
 
       $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
       $response = $this->request($method, $url, $request_options);
-      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The datetime value '{$value}' did not parse properly for the format 'Y-m-d'\n{$fieldName}.0.value: This value should be of the correct primitive type.\n";
+      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\" (date-only).";
       $this->assertResourceErrorResponse(422, $message, $response);
     }
   }
diff --git a/core/modules/datetime/tests/src/Functional/EntityResource/EntityTest/EntityTestDatetimeTest.php b/core/modules/datetime/tests/src/Functional/EntityResource/EntityTest/EntityTestDatetimeTest.php
index cdd4d605d0d7f9631adc2ef2c5bbed8132132816..f5efce53f383a21d355df829766920ba14c3a4e4 100644
--- a/core/modules/datetime/tests/src/Functional/EntityResource/EntityTest/EntityTestDatetimeTest.php
+++ b/core/modules/datetime/tests/src/Functional/EntityResource/EntityTest/EntityTestDatetimeTest.php
@@ -90,7 +90,7 @@ protected function getExpectedNormalizedEntity() {
     return parent::getExpectedNormalizedEntity() + [
       static::$fieldName => [
         [
-          'value' => $this->entity->get(static::$fieldName)->value,
+          'value' => '2017-03-02T07:02:00+11:00',
         ],
       ],
     ];
@@ -103,6 +103,24 @@ protected function getNormalizedPostEntity() {
     return parent::getNormalizedPostEntity() + [
       static::$fieldName => [
         [
+          'value' => static::$dateString . '+00:00',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getNormalizedPatchEntity() {
+    return parent::getNormalizedPostEntity() + [
+      static::$fieldName => [
+        [
+          // Omitting the timezone is allowed, this should result in the site's
+          // timezone being used automatically. This does not make sense, but
+          // it's how it functioned in the past, so we explicitly test this to
+          // guarantee backward compatibility. ::getNormalizedPostEntity() tests
+          // the recommended case, this tests backward compatibility.
           'value' => static::$dateString,
         ],
       ],
@@ -136,7 +154,7 @@ protected function assertNormalizationEdgeCases($method, Url $url, array $reques
 
       $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
       $response = $this->request($method, $url, $request_options);
-      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The datetime value '{$value}' is invalid for the format 'Y-m-d\\TH:i:s'\n";
+      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\\TH:i:sP\" (RFC 3339), \"Y-m-d\\TH:i:sO\" (ISO 8601), \"Y-m-d\\TH:i:s\" (backward compatibility — deprecated).";
       $this->assertResourceErrorResponse(422, $message, $response);
 
       // DX: 422 when date format is incorrect.
@@ -146,9 +164,29 @@ protected function assertNormalizationEdgeCases($method, Url $url, array $reques
 
       $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
       $response = $this->request($method, $url, $request_options);
-      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0: The datetime value '{$value}' did not parse properly for the format 'Y-m-d\\TH:i:s'\n{$fieldName}.0.value: This value should be of the correct primitive type.\n";
+      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\\TH:i:sP\" (RFC 3339), \"Y-m-d\\TH:i:sO\" (ISO 8601), \"Y-m-d\\TH:i:s\" (backward compatibility — deprecated).";
+      $this->assertResourceErrorResponse(422, $message, $response);
+
+      // DX: 422 when date value is invalid.
+      $normalization = $this->getNormalizedPostEntity();
+      $value = '2017-13-55T20:02:00+00:00';
+      $normalization[static::$fieldName][0]['value'] = $value;
+
+      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
+      $response = $this->request($method, $url, $request_options);
+      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\\TH:i:sP\" (RFC 3339), \"Y-m-d\\TH:i:sO\" (ISO 8601), \"Y-m-d\\TH:i:s\" (backward compatibility — deprecated).";
       $this->assertResourceErrorResponse(422, $message, $response);
     }
   }
 
+  /**
+   * {@inheritdoc}
+   *
+   * @group legacy
+   * @expectedDeprecation The provided datetime string format (Y-m-d\TH:i:s) is deprecated and will be removed before Drupal 9.0.0. Use the RFC3339 format instead (Y-m-d\TH:i:sP).
+   */
+  public function testPatch() {
+    return parent::testPatch();
+  }
+
 }
diff --git a/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDateRangeTest.php b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDateRangeTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e669a22371097b9f1e19a0a8ef60b485f6bc7955
--- /dev/null
+++ b/core/modules/datetime_range/tests/src/Functional/EntityResource/EntityTest/EntityTestDateRangeTest.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace Drupal\Tests\datetime_range\Functional\EntityResource\EntityTest;
+
+use Drupal\Core\Url;
+use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Tests\entity_test\Functional\Rest\EntityTestResourceTestBase;
+use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
+use GuzzleHttp\RequestOptions;
+
+/**
+ * Tests the 'daterange' field's normalization.
+ *
+ * @group datetime_range
+ */
+class EntityTestDateRangeTest extends EntityTestResourceTestBase {
+
+  use AnonResourceTestTrait;
+
+  /**
+   * The ISO date string to use throughout the test.
+   *
+   * @var string
+   */
+  protected static $dateString = '2017-03-01T20:02:00';
+
+  /**
+   * Datetime Range test field name.
+   *
+   * @var string
+   */
+  protected static $fieldName = 'field_daterange';
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['datetime_range', 'entity_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Add datetime_range field.
+    FieldStorageConfig::create([
+      'field_name' => static::$fieldName,
+      'type' => 'daterange',
+      'entity_type' => static::$entityTypeId,
+      'settings' => ['datetime_type' => DateRangeItem::DATETIME_TYPE_ALLDAY],
+    ])->save();
+
+    FieldConfig::create([
+      'field_name' => static::$fieldName,
+      'entity_type' => static::$entityTypeId,
+      'bundle' => $this->entity->bundle(),
+    ])->save();
+
+    // Reload entity so that it has the new field.
+    $this->entity = $this->entityStorage->load($this->entity->id());
+    $this->entity->set(static::$fieldName, [
+      'value' => static::$dateString,
+      'end_value' => static::$dateString,
+    ]);
+    $this->entity->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createEntity() {
+    $entity_test = EntityTest::create([
+      'name' => 'Llama',
+      'type' => static::$entityTypeId,
+    ]);
+    $entity_test->setOwnerId(0);
+    $entity_test->save();
+
+    return $entity_test;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedNormalizedEntity() {
+    return parent::getExpectedNormalizedEntity() + [
+      static::$fieldName => [
+        [
+          'value' => '2017-03-02T07:02:00+11:00',
+          'end_value' => '2017-03-02T07:02:00+11:00',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getNormalizedPostEntity() {
+    return parent::getNormalizedPostEntity() + [
+      static::$fieldName => [
+        [
+          'value' => '2017-03-01T20:02:00+00:00',
+          'end_value' => '2017-03-01T20:02:00+00:00',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {
+    parent::assertNormalizationEdgeCases($method, $url, $request_options);
+
+    if ($this->entity->getEntityType()->hasKey('bundle')) {
+      $fieldName = static::$fieldName;
+
+      // DX: 422 when 'value' data type is incorrect.
+      $normalization = $this->getNormalizedPostEntity();
+      $normalization[static::$fieldName][0]['value'] = [
+        '2017', '03', '01', '21', '53', '00',
+      ];
+      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
+      $response = $this->request($method, $url, $request_options);
+      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0.value: This value should be of the correct primitive type.\n";
+      $this->assertResourceErrorResponse(422, $message, $response);
+
+      // DX: 422 when 'end_value' is not specified.
+      $normalization = $this->getNormalizedPostEntity();
+      unset($normalization[static::$fieldName][0]['end_value']);
+      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
+      $response = $this->request($method, $url, $request_options);
+      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0.end_value: This value should not be null.\n";
+      $this->assertResourceErrorResponse(422, $message, $response);
+
+      // DX: 422 when 'end_value' data type is incorrect.
+      $normalization = $this->getNormalizedPostEntity();
+      $normalization[static::$fieldName][0]['end_value'] = [
+        '2017', '03', '01', '21', '53', '00',
+      ];
+      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
+      $response = $this->request($method, $url, $request_options);
+      $message = "Unprocessable Entity: validation failed.\n{$fieldName}.0.end_value: This value should be of the correct primitive type.\n";
+      $this->assertResourceErrorResponse(422, $message, $response);
+
+      // DX: 422 when end date value is invalid.
+      $normalization = $this->getNormalizedPostEntity();
+      $value = '2017-13-55T20:02:00+00:00';
+      $normalization[static::$fieldName][0]['end_value'] = $value;
+
+      $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
+      $response = $this->request($method, $url, $request_options);
+      $message = "The specified date \"$value\" is not in an accepted format: \"Y-m-d\\TH:i:sP\" (RFC 3339), \"Y-m-d\\TH:i:sO\" (ISO 8601), \"Y-m-d\\TH:i:s\" (backward compatibility — deprecated).";
+      $this->assertResourceErrorResponse(422, $message, $response);
+
+      // @todo Expand in https://www.drupal.org/project/drupal/issues/2847041.
+    }
+  }
+
+}
diff --git a/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php b/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php
index 6c47e1832127a7486fb0b7fd096b72c8bead51db..afb905fea9c0b5227426b31668413b27b5966af6 100644
--- a/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/TimestampItemNormalizer.php
@@ -4,15 +4,18 @@
 
 use Drupal\Core\Field\FieldItemInterface;
 use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
-use Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait;
+use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
 
 /**
  * Converts values for TimestampItem to and from common formats for hal.
+ *
+ * Overrides FieldItemNormalizer to
+ * - during normalization, add the 'format' key to assist consumers
+ * - during denormalization, use
+ *   \Drupal\serialization\Normalizer\TimestampNormalizer
  */
 class TimestampItemNormalizer extends FieldItemNormalizer {
 
-  use TimeStampItemNormalizerTrait;
-
   /**
    * {@inheritdoc}
    */
@@ -22,8 +25,21 @@ class TimestampItemNormalizer extends FieldItemNormalizer {
    * {@inheritdoc}
    */
   protected function normalizedFieldValues(FieldItemInterface $field_item, $format, array $context) {
-    $normalized = parent::normalizedFieldValues($field_item, $format, $context);
-    return $this->processNormalizedValues($normalized);
+    return parent::normalizedFieldValues($field_item, $format, $context) + [
+      // 'format' is not a property on Timestamp objects. This is present to
+      // assist consumers of this data.
+      'format' => \DateTime::RFC3339,
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function constructValue($data, $context) {
+    if (!empty($data['format'])) {
+      $context['datetime_allowed_formats'] = [$data['format']];
+    }
+    return ['value' => $this->serializer->denormalize($data['value'], Timestamp::class, NULL, $context)];
   }
 
 }
diff --git a/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php b/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php
index a275f646f7426a519506cbc458f83b8408f772da..506db71e1c695792b17c092b00d6a81bc19ee2cd 100644
--- a/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php
+++ b/core/modules/rest/tests/src/Functional/BcTimestampNormalizerUnixTestTrait.php
@@ -31,6 +31,8 @@ protected function formatExpectedTimestampItemValues($timestamp) {
     // \Drupal\serialization\Normalizer\TimestampItemNormalizer will produce.
     $date = new \DateTime();
     $date->setTimestamp($timestamp);
+    // Per \Drupal\Core\TypedData\Plugin\DataType\Timestamp::getDateTime(), they
+    // default to string representations in the UTC timezone.
     $date->setTimezone(new \DateTimeZone('UTC'));
 
     // Format is also added to the expected return values.
diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml
index 324a826e4f530e019567c2ad0decc599c314e5fa..bc316200ad41ad71a3f9decb0eefb87ba3885e76 100644
--- a/core/modules/serialization/serialization.services.yml
+++ b/core/modules/serialization/serialization.services.yml
@@ -58,6 +58,18 @@ services:
       # Priority must be higher than serializer.normalizer.field_item and lower
       # than hal normalizers.
       - { name: normalizer, priority: 8, bc: bc_timestamp_normalizer_unix, bc_config_name: 'serialization.settings' }
+  serializer.normalizer.timestamp:
+    class: Drupal\serialization\Normalizer\TimestampNormalizer
+    arguments: ['@config.factory']
+    tags:
+      # Priority must be higher than serializer.normalizer.primitive_data.
+      - { name: normalizer, priority: 20, bc: bc_timestamp_normalizer_unix, bc_config_name: 'serialization.settings' }
+  serializer.normalizer.datetimeiso8601:
+    class: \Drupal\serialization\Normalizer\DateTimeIso8601Normalizer
+    arguments: ['@config.factory']
+    tags:
+      # Priority must be higher than serializer.normalizer.primitive_data.
+      - { name: normalizer, priority: 20 }
   serializer.normalizer.password_field_item:
       class: Drupal\serialization\Normalizer\NullNormalizer
       arguments: ['Drupal\Core\Field\Plugin\Field\FieldType\PasswordItem']
diff --git a/core/modules/serialization/src/Normalizer/DateTimeIso8601Normalizer.php b/core/modules/serialization/src/Normalizer/DateTimeIso8601Normalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..fc6cf9f33adfa0824e4c312c62f14ea658a555ab
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/DateTimeIso8601Normalizer.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
+use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+
+/**
+ * Converts values for the DateTimeIso8601 data type to RFC3339.
+ *
+ * @internal
+ */
+class DateTimeIso8601Normalizer extends DateTimeNormalizer {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $allowedFormats = [
+    'RFC 3339' => \DateTime::RFC3339,
+    'ISO 8601' => \DateTime::ISO8601,
+    // @todo Remove this in https://www.drupal.org/project/drupal/issues/2958416.
+    // RFC3339 only covers combined date and time representations. For date-only
+    // representations, we need to use ISO 8601. There isn't a constant on the
+    // \DateTime class that we can use, so we have to hardcode the format.
+    // @see https://en.wikipedia.org/wiki/ISO_8601#Calendar_dates
+    // @see \Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::DATE_STORAGE_FORMAT
+    'date-only' => 'Y-m-d',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = DateTimeIso8601::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($datetime, $format = NULL, array $context = []) {
+    assert($datetime instanceof DateTimeIso8601);
+    $field_item = $datetime->getParent();
+    // @todo Remove this in https://www.drupal.org/project/drupal/issues/2958416.
+    if ($field_item instanceof DateTimeItem && $field_item->getFieldDefinition()->getFieldStorageDefinition()->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) {
+      $drupal_date_time = $datetime->getDateTime();
+      if ($drupal_date_time === NULL) {
+        return $drupal_date_time;
+      }
+      return $drupal_date_time->format($this->allowedFormats['date-only']);
+    }
+    return parent::normalize($datetime, $format, $context);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    // @todo Move the date-only handling out of here in https://www.drupal.org/project/drupal/issues/2958416.
+    $field_definition = isset($context['target_instance'])
+      ? $context['target_instance']->getFieldDefinition()
+      : (isset($context['field_definition']) ? $context['field_definition'] : NULL);
+    if ($field_definition === NULL) {
+      throw new InvalidArgumentException('$context[\'target_instance\'] or $context[\'field_definition\'] must be set to denormalize with the DateTimeIso8601Normalizer');
+    }
+
+    $datetime_type = $field_definition->getSetting('datetime_type');
+    $is_date_only = $datetime_type === DateTimeItem::DATETIME_TYPE_DATE;
+
+    if ($is_date_only) {
+      $context['datetime_allowed_formats'] = array_intersect_key($this->allowedFormats, ['date-only' => TRUE]);
+      $datetime = parent::denormalize($data, $class, $format, $context);
+      if (!$datetime instanceof \DateTime) {
+        return $datetime;
+      }
+      return $datetime->format(DateTimeItemInterface::DATE_STORAGE_FORMAT);
+    }
+
+    $context['datetime_allowed_formats'] = array_diff_key($this->allowedFormats, ['date-only' => TRUE]);
+    try {
+      $datetime = parent::denormalize($data, $class, $format, $context);
+    }
+    catch (\UnexpectedValueException $e) {
+      // If denormalization didn't work using any of the actively supported
+      // formats, try again with the BC format too. Explicitly label it as
+      // being deprecated and trigger a deprecation error.
+      $using_deprecated_format = TRUE;
+      $context['datetime_allowed_formats']['backward compatibility — deprecated'] = DateTimeItemInterface::DATETIME_STORAGE_FORMAT;
+      $datetime = parent::denormalize($data, $class, $format, $context);
+    }
+    if (!$datetime instanceof \DateTime) {
+      return $datetime;
+    }
+    if (isset($using_deprecated_format)) {
+      @trigger_error('The provided datetime string format (Y-m-d\\TH:i:s) is deprecated and will be removed before Drupal 9.0.0. Use the RFC3339 format instead (Y-m-d\\TH:i:sP).', E_USER_DEPRECATED);
+    }
+    $datetime->setTimezone(new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE));
+    return $datetime->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
+  }
+
+}
diff --git a/core/modules/serialization/src/Normalizer/DateTimeNormalizer.php b/core/modules/serialization/src/Normalizer/DateTimeNormalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..209d15201b5d20b4f74e3f0dfa6eb94e8b5b50a2
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/DateTimeNormalizer.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\TypedData\Type\DateTimeInterface;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+/**
+ * Converts values for datetime objects to RFC3339 and from common formats.
+ *
+ * @internal
+ */
+class DateTimeNormalizer extends NormalizerBase implements DenormalizerInterface {
+
+  /**
+   * Allowed datetime formats for the denormalizer.
+   *
+   * The list is chosen to be unambiguous and language neutral, but also common
+   * for data interchange.
+   *
+   * @var string[]
+   *
+   * @see http://php.net/manual/en/datetime.createfromformat.php
+   */
+  protected $allowedFormats = [
+    'RFC 3339' => \DateTime::RFC3339,
+    'ISO 8601' => \DateTime::ISO8601,
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = DateTimeInterface::class;
+
+  /**
+   * The system's date configuration.
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected $systemDateConfig;
+
+  /**
+   * Constructs a new DateTimeNormalizer instance.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   A config factory for retrieving required config objects.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory) {
+    $this->systemDateConfig = $config_factory->get('system.date');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($datetime, $format = NULL, array $context = []) {
+    assert($datetime instanceof DateTimeInterface);
+    $drupal_date_time = $datetime->getDateTime();
+    if ($drupal_date_time === NULL) {
+      return $drupal_date_time;
+    }
+    return $drupal_date_time
+      // Set an explicit timezone. Otherwise, timestamps may end up being
+      // normalized using the user's preferred timezone. Which would result in
+      // many variations and complex caching.
+      // @see \Drupal\Core\Datetime\DrupalDateTime::prepareTimezone()
+      // @see drupal_get_user_timezone()
+      ->setTimezone($this->getNormalizationTimezone())
+      ->format(\DateTime::RFC3339);
+  }
+
+  /**
+   * Gets the timezone to be used during normalization.
+   *
+   * @see ::normalize
+   *
+   * @returns \DateTimeZone
+   *   The timezone to use.
+   */
+  protected function getNormalizationTimezone() {
+    $default_site_timezone = $this->systemDateConfig->get('timezone.default');
+    return new \DateTimeZone($default_site_timezone);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    // This only knows how to denormalize datetime strings and timestamps. If
+    // something else is received, let validation constraints handle this.
+    if (!is_string($data) && !is_numeric($data)) {
+      return $data;
+    }
+
+    // Loop through the allowed formats and create a \DateTime from the
+    // input data if it matches the defined pattern. Since the formats are
+    // unambiguous (i.e., they reference an absolute time with a defined time
+    // zone), only one will ever match.
+    $allowed_formats = isset($context['datetime_allowed_formats'])
+      ? $context['datetime_allowed_formats']
+      : $this->allowedFormats;
+    foreach ($allowed_formats as $format) {
+      $date = \DateTime::createFromFormat($format, $data);
+      $errors = \DateTime::getLastErrors();
+      if ($date !== FALSE && empty($errors['errors']) && empty($errors['warnings'])) {
+        return $date;
+      }
+    }
+
+    $format_strings = [];
+
+    foreach ($allowed_formats as $label => $format) {
+      $format_strings[] = "\"$format\" ($label)";
+    }
+
+    $formats = implode(', ', $format_strings);
+    throw new UnexpectedValueException(sprintf('The specified date "%s" is not in an accepted format: %s.', $data, $formats));
+  }
+
+}
diff --git a/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php b/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php
index 1ad0d8e66be785995364322d8559abbc42b99ac8..50296711f3e86667dc2db96e7c962d1b3b00be65 100644
--- a/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php
+++ b/core/modules/serialization/src/Normalizer/TimeStampItemNormalizerTrait.php
@@ -4,8 +4,12 @@
 
 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 
+@trigger_error(__NAMESPACE__ . '\TimeStampItemNormalizerTrait is deprecated in Drupal 8.7.0 and will be removed in Drupal 9.0.0. Use \Drupal\serialization\Normalizer\TimestampNormalizer instead.', E_USER_DEPRECATED);
+
 /**
  * A trait for TimestampItem normalization functionality.
+ *
+ * @deprecated in 8.7.0, use \Drupal\serialization\Normalizer\TimestampNormalizer instead.
  */
 trait TimeStampItemNormalizerTrait {
 
diff --git a/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php b/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php
index 484b8347ad9ede7daf644f7bcae320e1d5dc03fd..1669e6a7803b87446c91f1e03c4d7e5bfa6ceaf5 100644
--- a/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/TimestampItemNormalizer.php
@@ -3,15 +3,19 @@
 namespace Drupal\serialization\Normalizer;
 
 use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
-use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
 
 /**
  * Converts values for TimestampItem to and from common formats.
+ *
+ * Overrides FieldItemNormalizer to use \Drupal\serialization\Normalizer\TimestampNormalizer
+ *
+ * Overrides FieldItemNormalizer to
+ * - during normalization, add the 'format' key to assist consumers
+ * - during denormalization, use \Drupal\serialization\Normalizer\TimestampNormalizer
  */
 class TimestampItemNormalizer extends FieldItemNormalizer {
 
-  use TimeStampItemNormalizerTrait;
-
   /**
    * {@inheritdoc}
    */
@@ -20,21 +24,22 @@ class TimestampItemNormalizer extends FieldItemNormalizer {
   /**
    * {@inheritdoc}
    */
-  public function normalize($field_item, $format = NULL, array $context = []) {
-    $data = parent::normalize($field_item, $format, $context);
-
-    return $this->processNormalizedValues($data);
+  public function normalize($object, $format = NULL, array $context = []) {
+    return parent::normalize($object, $format, $context) + [
+      // 'format' is not a property on Timestamp objects. This is present to
+      // assist consumers of this data.
+      'format' => \DateTime::RFC3339,
+    ];
   }
 
   /**
    * {@inheritdoc}
    */
-  public function denormalize($data, $class, $format = NULL, array $context = []) {
-    if (empty($data['value'])) {
-      throw new InvalidArgumentException('No "value" attribute present');
+  protected function constructValue($data, $context) {
+    if (!empty($data['format'])) {
+      $context['datetime_allowed_formats'] = [$data['format']];
     }
-
-    return parent::denormalize($data, $class, $format, $context);
+    return ['value' => $this->serializer->denormalize($data['value'], Timestamp::class, NULL, $context)];
   }
 
 }
diff --git a/core/modules/serialization/src/Normalizer/TimestampNormalizer.php b/core/modules/serialization/src/Normalizer/TimestampNormalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..9be4d6ef949b7152e69c984d3a4e66caf5ff7916
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/TimestampNormalizer.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
+
+/**
+ * Converts values for the Timestamp data type to and from common formats.
+ *
+ * @internal
+ *
+ * Note that \Drupal\Core\TypedData\Plugin\DataType\Timestamp::getDateTime()
+ * explicitly sets a default timezone of UTC. This ensures the string
+ * representation generated by DateTimeNormalizer::normalize() is also in UTC.
+ */
+class TimestampNormalizer extends DateTimeNormalizer {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $allowedFormats = [
+    'UNIX timestamp' => 'U',
+    'ISO 8601' => \DateTime::ISO8601,
+    'RFC 3339' => \DateTime::RFC3339,
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = Timestamp::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getNormalizationTimezone() {
+    return new \DateTimeZone('UTC');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    $denormalized = parent::denormalize($data, $class, $format, $context);
+    return $denormalized->getTimestamp();
+  }
+
+}
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/DateTimeIso8601NormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/DateTimeIso8601NormalizerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..188d0b54cd6466355006faf1fe976a866e1c1eaf
--- /dev/null
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/DateTimeIso8601NormalizerTest.php
@@ -0,0 +1,283 @@
+<?php
+
+namespace Drupal\Tests\serialization\Unit\Normalizer;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\ImmutableConfig;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemBase;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601;
+use Drupal\Core\TypedData\Plugin\DataType\IntegerData;
+use Drupal\Core\TypedData\Type\DateTimeInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
+use Drupal\serialization\Normalizer\DateTimeIso8601Normalizer;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
+/**
+ * Unit test coverage for the "datetime_iso8601" @DataType.
+ *
+ * @coversDefaultClass \Drupal\serialization\Normalizer\DateTimeIso8601Normalizer
+ * @group serialization
+ * @see \Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601
+ * @see \Drupal\datetime\Plugin\Field\FieldType\DateTimeItem::DATETIME_TYPE_DATE
+ */
+class DateTimeIso8601NormalizerTest extends UnitTestCase {
+
+  /**
+   * The tested data type's normalizer.
+   *
+   * @var \Drupal\serialization\Normalizer\DateTimeIso8601Normalizer
+   */
+  protected $normalizer;
+
+  /**
+   * The tested data type.
+   *
+   * @var \Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601
+   */
+  protected $data;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $system_date_config = $this->prophesize(ImmutableConfig::class);
+    $system_date_config->get('timezone.default')
+      ->willReturn('Australia/Sydney');
+    $config_factory = $this->prophesize(ConfigFactoryInterface::class);
+    $config_factory->get('system.date')
+      ->willReturn($system_date_config->reveal());
+
+    $this->normalizer = new DateTimeIso8601Normalizer($config_factory->reveal());
+    $this->data = $this->prophesize(DateTimeIso8601::class);
+  }
+
+  /**
+   * @covers ::supportsNormalization
+   */
+  public function testSupportsNormalization() {
+    $this->assertTrue($this->normalizer->supportsNormalization($this->data->reveal()));
+
+    $datetime = $this->prophesize(DateTimeInterface::class);
+    $this->assertFalse($this->normalizer->supportsNormalization($datetime->reveal()));
+
+    $integer = $this->prophesize(IntegerData::class);
+    $this->assertFalse($this->normalizer->supportsNormalization($integer->reveal()));
+  }
+
+  /**
+   * @covers ::supportsDenormalization
+   */
+  public function testSupportsDenormalization() {
+    $this->assertTrue($this->normalizer->supportsDenormalization($this->data->reveal(), DateTimeIso8601::class));
+  }
+
+  /**
+   * @covers ::normalize
+   * @dataProvider providerTestNormalize
+   */
+  public function testNormalize($parent_field_item_class, $datetime_type, $expected_format) {
+    $formatted_string = $this->randomMachineName();
+
+    $field_item = $this->prophesize($parent_field_item_class);
+    if ($parent_field_item_class === DateTimeItem::class) {
+      $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class);
+      $field_storage_definition->getSetting('datetime_type')
+        ->willReturn($datetime_type);
+      $field_definition = $this->prophesize(FieldDefinitionInterface::class);
+      $field_definition->getFieldStorageDefinition()
+        ->willReturn($field_storage_definition);
+      $field_item->getFieldDefinition()
+        ->willReturn($field_definition);
+    }
+    else {
+      $field_item->getFieldDefinition(Argument::any())
+        ->shouldNotBeCalled();
+    }
+    $this->data->getParent()
+      ->willReturn($field_item);
+
+    $drupal_date_time = $this->prophesize(DateTimeIso8601NormalizerTestDrupalDateTime::class);
+    $drupal_date_time->setTimezone(new \DateTimeZone('Australia/Sydney'))
+      ->willReturn($drupal_date_time->reveal());
+    $drupal_date_time->format($expected_format)
+      ->willReturn($formatted_string);
+    $this->data->getDateTime()
+      ->willReturn($drupal_date_time->reveal());
+
+    $normalized = $this->normalizer->normalize($this->data->reveal());
+    $this->assertSame($formatted_string, $normalized);
+  }
+
+  /**
+   * @covers ::normalize
+   * @dataProvider providerTestNormalize
+   */
+  public function testNormalizeWhenNull($parent_field_item_class, $datetime_type, $expected_format) {
+    $field_item = $this->prophesize($parent_field_item_class);
+    if ($parent_field_item_class === DateTimeItem::class) {
+      $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class);
+      $field_storage_definition->getSetting('datetime_type')
+        ->willReturn($datetime_type);
+      $field_definition = $this->prophesize(FieldDefinitionInterface::class);
+      $field_definition->getFieldStorageDefinition()
+        ->willReturn($field_storage_definition);
+      $field_item->getFieldDefinition()
+        ->willReturn($field_definition);
+    }
+    else {
+      $field_item->getFieldDefinition(Argument::any())
+        ->shouldNotBeCalled();
+    }
+    $this->data->getParent()
+      ->willReturn($field_item);
+
+    $this->data->getDateTime()
+      ->willReturn(NULL);
+
+    $normalized = $this->normalizer->normalize($this->data->reveal());
+    $this->assertNull($normalized);
+  }
+
+  /**
+   * Data provider for testNormalize.
+   *
+   * @return array
+   */
+  public function providerTestNormalize() {
+    return [
+      // @see \Drupal\datetime\Plugin\Field\FieldType\DateTimeItem::DATETIME_TYPE_DATE
+      'datetime field, configured to store only date: must be handled by DateTimeIso8601Normalizer' => [
+        DateTimeItem::class,
+        DateTimeItem::DATETIME_TYPE_DATE,
+        // This expected format call proves that normalization is handled by \Drupal\serialization\Normalizer\DateTimeIso8601Normalizer::normalize().
+        'Y-m-d',
+      ],
+      // @see \Drupal\datetime\Plugin\Field\FieldType\DateTimeItem::DATETIME_TYPE_DATETIME
+      'datetime field, configured to store date and time; must be handled by the parent normalizer' => [
+        DateTimeItem::class,
+        DateTimeItem::DATETIME_TYPE_DATETIME,
+        \DateTime::RFC3339,
+      ],
+      'non-datetime field; must be handled by the parent normalizer' => [
+        FieldItemBase::class,
+        NULL,
+        \DateTime::RFC3339,
+      ],
+
+    ];
+  }
+
+  /**
+   * Tests the denormalize function with good data.
+   *
+   * @covers ::denormalize
+   * @dataProvider providerTestDenormalizeValidFormats
+   */
+  public function testDenormalizeValidFormats($type, $normalized, $expected) {
+    $field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $field_definition->getSetting('datetime_type')->willReturn($type === 'date-only' ? DateTimeItem::DATETIME_TYPE_DATE : DateTimeItem::DATETIME_TYPE_DATETIME);
+    $denormalized = $this->normalizer->denormalize($normalized, DateTimeIso8601::class, NULL, [
+      'field_definition' => $field_definition->reveal(),
+    ]);
+    $this->assertSame($expected, $denormalized);
+  }
+
+  /**
+   * Data provider for testDenormalizeValidFormats.
+   *
+   * @return array
+   */
+  public function providerTestDenormalizeValidFormats() {
+    $data = [];
+    $data['just a date'] = ['date-only', '2016-11-06', '2016-11-06'];
+
+    $data['RFC3339'] = ['date+time', '2016-11-06T09:02:00+00:00', '2016-11-06T09:02:00'];
+    $data['RFC3339 +0100'] = ['date+time', '2016-11-06T09:02:00+01:00', '2016-11-06T08:02:00'];
+    $data['RFC3339 -0600'] = ['date+time', '2016-11-06T09:02:00-06:00', '2016-11-06T15:02:00'];
+
+    $data['ISO8601'] = ['date+time', '2016-11-06T09:02:00+0000', '2016-11-06T09:02:00'];
+    $data['ISO8601 +0100'] = ['date+time', '2016-11-06T09:02:00+0100', '2016-11-06T08:02:00'];
+    $data['ISO8601 -0600'] = ['date+time', '2016-11-06T09:02:00-0600', '2016-11-06T15:02:00'];
+
+    return $data;
+  }
+
+  /**
+   * Tests the denormalize function with the date+time deprecated format.
+   *
+   * @covers ::denormalize
+   * @group legacy
+   * @expectedDeprecation The provided datetime string format (Y-m-d\TH:i:s) is deprecated and will be removed before Drupal 9.0.0. Use the RFC3339 format instead (Y-m-d\TH:i:sP).
+   */
+  public function testDenormalizeDateAndTimeDeprecatedFormat() {
+    $normalized = '2016-11-06T08:00:00';
+
+    $field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $field_definition->getSetting('datetime_type')->willReturn(DateTimeItem::DATETIME_TYPE_DATETIME);
+    $this->normalizer->denormalize($normalized, DateTimeIso8601::class, NULL, ['field_definition' => $field_definition->reveal()]);
+  }
+
+  /**
+   * Tests the denormalize function with bad data for the date-only case.
+   *
+   * @covers ::denormalize
+   */
+  public function testDenormalizeDateOnlyException() {
+    $this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06" is not in an accepted format: "Y-m-d" (date-only).');
+
+    $normalized = '2016/11/06';
+
+    $field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $field_definition->getSetting('datetime_type')->willReturn(DateTimeItem::DATETIME_TYPE_DATE);
+    $this->normalizer->denormalize($normalized, DateTimeIso8601::class, NULL, ['field_definition' => $field_definition->reveal()]);
+  }
+
+  /**
+   * Tests the denormalize function with bad data for the date+time case.
+   *
+   * @covers ::denormalize
+   */
+  public function testDenormalizeDateAndTimeException() {
+    $this->setExpectedException(UnexpectedValueException::class, 'The specified date "on a rainy day" is not in an accepted format: "Y-m-d\TH:i:sP" (RFC 3339), "Y-m-d\TH:i:sO" (ISO 8601), "Y-m-d\TH:i:s" (backward compatibility — deprecated).');
+
+    $normalized = 'on a rainy day';
+
+    $field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $field_definition->getSetting('datetime_type')->willReturn(DateTimeItem::DATETIME_TYPE_DATETIME);
+    $this->normalizer->denormalize($normalized, DateTimeIso8601::class, NULL, ['field_definition' => $field_definition->reveal()]);
+  }
+
+  /**
+   * Tests the denormalize function with incomplete serialization context.
+   *
+   * @covers ::denormalize
+   */
+  public function testDenormalizeNoTargetInstanceOrFieldDefinitionException() {
+    $this->setExpectedException(InvalidArgumentException::class, '$context[\'target_instance\'] or $context[\'field_definition\'] must be set to denormalize with the DateTimeIso8601Normalizer');
+    $this->normalizer->denormalize('', DateTimeIso8601::class, NULL, []);
+  }
+
+}
+
+/**
+ * Note: Prophecy does not support magic methods. By subclassing and specifying
+ * an explicit method, Prophecy works.
+ * @see https://github.com/phpspec/prophecy/issues/338
+ * @see https://github.com/phpspec/prophecy/issues/34
+ * @see https://github.com/phpspec/prophecy/issues/80
+ */
+class DateTimeIso8601NormalizerTestDrupalDateTime extends DrupalDateTime {
+
+  public function setTimezone(\DateTimeZone $timezone) {
+    parent::setTimezone($timezone);
+  }
+
+}
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/DateTimeNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/DateTimeNormalizerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..8914186e244c13ff71b51e0c958658f210e3e573
--- /dev/null
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/DateTimeNormalizerTest.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace Drupal\Tests\serialization\Unit\Normalizer;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\ImmutableConfig;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601;
+use Drupal\Core\TypedData\Plugin\DataType\IntegerData;
+use Drupal\Core\TypedData\Type\DateTimeInterface;
+use Drupal\serialization\Normalizer\DateTimeNormalizer;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
+/**
+ * Unit test coverage for @DataTypes implementing DateTimeInterface.
+ *
+ * @group serialization
+ * @coversDefaultClass \Drupal\serialization\Normalizer\DateTimeNormalizer
+ * @see \Drupal\Core\TypedData\Type\DateTimeInterface
+ */
+class DateTimeNormalizerTest extends UnitTestCase {
+
+  /**
+   * The tested data type's normalizer.
+   *
+   * @var \Drupal\serialization\Normalizer\DateTimeNormalizer
+   */
+  protected $normalizer;
+
+  /**
+   * The tested data type.
+   *
+   * @var \Drupal\Core\TypedData\Type\DateTimeInterface
+   */
+  protected $data;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $system_date_config = $this->prophesize(ImmutableConfig::class);
+    $system_date_config->get('timezone.default')
+      ->willReturn('Australia/Sydney');
+    $config_factory = $this->prophesize(ConfigFactoryInterface::class);
+    $config_factory->get('system.date')
+      ->willReturn($system_date_config->reveal());
+
+    $this->normalizer = new DateTimeNormalizer($config_factory->reveal());
+    $this->data = $this->prophesize(DateTimeInterface::class);
+  }
+
+  /**
+   * @covers ::supportsNormalization
+   */
+  public function testSupportsNormalization() {
+    $this->assertTrue($this->normalizer->supportsNormalization($this->data->reveal()));
+
+    $datetimeiso8601 = $this->prophesize(DateTimeIso8601::class);
+    $this->assertTrue($this->normalizer->supportsNormalization($datetimeiso8601->reveal()));
+
+    $integer = $this->prophesize(IntegerData::class);
+    $this->assertFalse($this->normalizer->supportsNormalization($integer->reveal()));
+  }
+
+  /**
+   * @covers ::supportsDenormalization
+   */
+  public function testSupportsDenormalization() {
+    $this->assertTrue($this->normalizer->supportsDenormalization($this->data->reveal(), DateTimeInterface::class));
+  }
+
+  /**
+   * @covers ::normalize
+   */
+  public function testNormalize() {
+    $random_rfc_3339_string = $this->randomMachineName();
+
+    $drupal_date_time = $this->prophesize(DateTimeNormalizerTestDrupalDateTime::class);
+    $drupal_date_time->setTimezone(new \DateTimeZone('Australia/Sydney'))
+      ->willReturn($drupal_date_time->reveal());
+    $drupal_date_time->format(\DateTime::RFC3339)
+      ->willReturn($random_rfc_3339_string);
+
+    $this->data->getDateTime()
+      ->willReturn($drupal_date_time->reveal());
+
+    $normalized = $this->normalizer->normalize($this->data->reveal());
+    $this->assertSame($random_rfc_3339_string, $normalized);
+  }
+
+  /**
+   * @covers ::normalize
+   */
+  public function testNormalizeWhenNull() {
+    $this->data->getDateTime()
+      ->willReturn(NULL);
+
+    $normalized = $this->normalizer->normalize($this->data->reveal());
+    $this->assertNull($normalized);
+  }
+
+  /**
+   * Tests the denormalize function with good data.
+   *
+   * @covers ::denormalize
+   * @dataProvider providerTestDenormalizeValidFormats
+   */
+  public function testDenormalizeValidFormats($normalized, $expected) {
+    $denormalized = $this->normalizer->denormalize($normalized, DateTimeInterface::class, NULL, []);
+    $this->assertSame(0, $denormalized->getTimestamp() - $expected->getTimestamp());
+    $this->assertEquals($expected, $denormalized);
+  }
+
+  /**
+   * Data provider for testDenormalizeValidFormats.
+   *
+   * @return array
+   */
+  public function providerTestDenormalizeValidFormats() {
+    $data = [];
+
+    $data['RFC3339'] = ['2016-11-06T09:02:00+00:00', new \DateTimeImmutable('2016-11-06T09:02:00+00:00')];
+    $data['RFC3339 +0100'] = ['2016-11-06T09:02:00+01:00', new \DateTimeImmutable('2016-11-06T09:02:00+01:00')];
+    $data['RFC3339 -0600'] = ['2016-11-06T09:02:00-06:00', new \DateTimeImmutable('2016-11-06T09:02:00-06:00')];
+
+    $data['ISO8601'] = ['2016-11-06T09:02:00+0000', new \DateTimeImmutable('2016-11-06T09:02:00+00:00')];
+    $data['ISO8601 +0100'] = ['2016-11-06T09:02:00+0100', new \DateTimeImmutable('2016-11-06T09:02:00+01:00')];
+    $data['ISO8601 -0600'] = ['2016-11-06T09:02:00-0600', new \DateTimeImmutable('2016-11-06T09:02:00-06:00')];
+
+    return $data;
+  }
+
+  /**
+   * Tests the denormalize function with a user supplied format.
+   *
+   * @covers ::denormalize
+   * @dataProvider providerTestDenormalizeUserFormats
+   */
+  public function testDenormalizeUserFormats($normalized, $format, $expected) {
+    $denormalized = $this->normalizer->denormalize($normalized, DateTimeInterface::class, NULL, ['datetime_allowed_formats' => [$format]]);
+    $this->assertSame(0, $denormalized->getTimestamp() - $expected->getTimestamp());
+    $this->assertEquals($expected, $denormalized);
+  }
+
+  /**
+   * Data provider for testDenormalizeUserFormats.
+   *
+   * @return array
+   */
+  public function providerTestDenormalizeUserFormats() {
+    $data = [];
+
+    $data['Y/m/d H:i:s P'] = ['2016/11/06 09:02:00 +00:00', 'Y/m/d H:i:s P', new \DateTimeImmutable('2016-11-06T09:02:00+00:00')];
+    $data['H:i:s Y/m/d P'] = ['09:02:00 2016/11/06  +01:00', 'H:i:s Y/m/d P', new \DateTimeImmutable('2016-11-06T09:02:00+01:00')];
+    $data['Y/m/d H:i:s'] = ['09:02:00 2016/11/06', 'H:i:s Y/m/d', new \DateTimeImmutable('2016-11-06T09:02:00+11:00')];
+
+    return $data;
+  }
+
+  /**
+   * Tests the denormalize function with bad data.
+   *
+   * @covers ::denormalize
+   */
+  public function testDenormalizeException() {
+    $this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06 09:02am GMT" is not in an accepted format: "Y-m-d\TH:i:sP" (RFC 3339), "Y-m-d\TH:i:sO" (ISO 8601).');
+
+    $normalized = '2016/11/06 09:02am GMT';
+
+    $this->normalizer->denormalize($normalized, DateTimeInterface::class, NULL, []);
+  }
+
+}
+
+
+/**
+ * Note: Prophecy does not support magic methods. By subclassing and specifying
+ * an explicit method, Prophecy works.
+ * @see https://github.com/phpspec/prophecy/issues/338
+ * @see https://github.com/phpspec/prophecy/issues/34
+ * @see https://github.com/phpspec/prophecy/issues/80
+ */
+class DateTimeNormalizerTestDrupalDateTime extends DrupalDateTime {
+
+  public function setTimezone(\DateTimeZone $timezone) {
+    parent::setTimezone($timezone);
+  }
+
+}
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/TimeStampItemNormalizerTraitDeprecatedTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/TimeStampItemNormalizerTraitDeprecatedTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..77623a190fcfb7c8026796f83a65a9d7bfb06bea
--- /dev/null
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimeStampItemNormalizerTraitDeprecatedTest.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\Tests\serialization\Unit\Normalizer;
+
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests that TimeStampItemNormalizerTrait throws a deprecation error.
+ *
+ * @group serialization
+ * @group legacy
+ * @coversDefaultClass \Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait
+ */
+class TimeStampItemNormalizerTraitDeprecatedTest extends UnitTestCase {
+
+  /**
+   * Tests that TimeStampItemNormalizerTrait throws a deprecation error.
+   *
+   * @expectedDeprecation Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait is deprecated in Drupal 8.7.0 and will be removed in Drupal 9.0.0. Use \Drupal\serialization\Normalizer\TimestampNormalizer instead.
+   */
+  public function testDeprecated() {
+    $test = new TimeStampItemNormalizerTraitDeprecatedTestClass();
+  }
+
+}
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/TimeStampItemNormalizerTraitDeprecatedTestClass.php b/core/modules/serialization/tests/src/Unit/Normalizer/TimeStampItemNormalizerTraitDeprecatedTestClass.php
new file mode 100644
index 0000000000000000000000000000000000000000..500414a03d4abb3e850e647e57bcbe075f9bb233
--- /dev/null
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimeStampItemNormalizerTraitDeprecatedTestClass.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace Drupal\Tests\serialization\Unit\Normalizer;
+
+use Drupal\serialization\Normalizer\TimeStampItemNormalizerTrait;
+
+/**
+ * For testing that TimeStampItemNormalizerTrait throws a deprecation error.
+ *
+ * @see \Drupal\Tests\serialization\Unit\Normalizer\TimeStampItemNormalizerTraitDeprecatedTest
+ */
+class TimeStampItemNormalizerTraitDeprecatedTestClass {
+  use TimeStampItemNormalizerTrait;
+
+}
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
index c4e351424ada16330017627072ff7c7da4f89f56..4d0127926e1da8b022e0b15079cc7acf31ba19f1 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampItemNormalizerTest.php
@@ -5,21 +5,21 @@
 use Drupal\Core\Field\Plugin\Field\FieldType\CreatedItem;
 use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
 use Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
 use Drupal\serialization\Normalizer\TimestampItemNormalizer;
 use Drupal\Tests\UnitTestCase;
-use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 use Symfony\Component\Serializer\Serializer;
 
 /**
- * Tests that entities can be serialized to supported core formats.
+ * Tests that TimestampItem (de)normalization uses Timestamp (de)normalization.
  *
  * @group serialization
  * @coversDefaultClass \Drupal\serialization\Normalizer\TimestampItemNormalizer
+ * @see \Drupal\serialization\Normalizer\TimestampNormalizer
  */
 class TimestampItemNormalizerTest extends UnitTestCase {
 
-  use InternalTypedDataTestTrait;
-
   /**
    * @var \Drupal\serialization\Normalizer\TimestampItemNormalizer
    */
@@ -68,88 +68,76 @@ public function testSupportsDenormalization() {
   }
 
   /**
-   * Tests the normalize function.
-   *
    * @covers ::normalize
+   * @see \Drupal\Tests\serialization\Unit\Normalizer\TimestampNormalizerTest
    */
   public function testNormalize() {
-    $expected = ['value' => '2016-11-06T09:02:00+00:00', 'format' => \DateTime::RFC3339];
-
+    // Mock TimestampItem @FieldType, which contains a Timestamp @DataType,
+    // which has a DataDefinition.
+    $data_definition = $this->prophesize(DataDefinitionInterface::class);
+    $data_definition->isInternal()
+      ->willReturn(FALSE)
+      ->shouldBeCalled();
+    $timestamp = $this->prophesize(Timestamp::class);
+    $timestamp->getDataDefinition()
+      ->willReturn($data_definition->reveal())
+      ->shouldBeCalled();
+    $timestamp = $timestamp->reveal();
     $timestamp_item = $this->createTimestampItemProphecy();
-    $timestamp_item->getIterator()
-      ->willReturn(new \ArrayIterator(['value' => 1478422920]));
-
-    $value_property = $this->getTypedDataProperty(FALSE);
     $timestamp_item->getProperties(TRUE)
-      ->willReturn(['value' => $value_property])
+      ->willReturn(['value' => $timestamp])
       ->shouldBeCalled();
 
+    // Mock Serializer service, to assert that the Timestamp @DataType
+    // normalizer would be called.
+    $timestamp_datetype_normalization = $this->randomMachineName();
     $serializer_prophecy = $this->prophesize(Serializer::class);
-
-    $serializer_prophecy->normalize($value_property, NULL, [])
-      ->willReturn(1478422920)
+    // This is where \Drupal\serialization\Normalizer\TimestampNormalizer would
+    // be called.
+    $serializer_prophecy->normalize($timestamp, NULL, [])
+      ->willReturn($timestamp_datetype_normalization)
       ->shouldBeCalled();
 
     $this->normalizer->setSerializer($serializer_prophecy->reveal());
 
     $normalized = $this->normalizer->normalize($timestamp_item->reveal());
-    $this->assertSame($expected, $normalized);
+    $this->assertSame(['value' => $timestamp_datetype_normalization, 'format' => \DateTime::RFC3339], $normalized);
   }
 
   /**
-   * Tests the denormalize function with good data.
-   *
    * @covers ::denormalize
-   * @dataProvider providerTestDenormalizeValidFormats
    */
-  public function testDenormalizeValidFormats($value, $expected) {
-    $normalized = ['value' => $value];
+  public function testDenormalize() {
+    $timestamp_item_normalization = [
+      'value' => $this->randomMachineName(),
+      'format' => \DateTime::RFC3339,
+    ];
+    $timestamp_data_denormalization = $this->randomMachineName();
 
     $timestamp_item = $this->createTimestampItemProphecy();
-    // The field item should be set with the expected timestamp.
-    $timestamp_item->setValue(['value' => $expected])
+    // The field item should get the Timestamp @DataType denormalization set as
+    // a value, in FieldItemNormalizer::denormalize().
+    $timestamp_item->setValue(['value' => $timestamp_data_denormalization])
       ->shouldBeCalled();
 
-    $context = ['target_instance' => $timestamp_item->reveal()];
-
-    $denormalized = $this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context);
-    $this->assertTrue($denormalized instanceof TimestampItem);
-  }
-
-  /**
-   * Data provider for testDenormalizeValidFormats.
-   *
-   * @return array
-   */
-  public function providerTestDenormalizeValidFormats() {
-    $expected_stamp = 1478422920;
-
-    $data = [];
-
-    $data['U'] = [$expected_stamp, $expected_stamp];
-    $data['RFC3339'] = ['2016-11-06T09:02:00+00:00', $expected_stamp];
-    $data['RFC3339 +0100'] = ['2016-11-06T09:02:00+01:00', $expected_stamp - 1 * 3600];
-    $data['RFC3339 -0600'] = ['2016-11-06T09:02:00-06:00', $expected_stamp + 6 * 3600];
-
-    $data['ISO8601'] = ['2016-11-06T09:02:00+0000', $expected_stamp];
-    $data['ISO8601 +0100'] = ['2016-11-06T09:02:00+0100', $expected_stamp - 1 * 3600];
-    $data['ISO8601 -0600'] = ['2016-11-06T09:02:00-0600', $expected_stamp + 6 * 3600];
+    $context = [
+      'target_instance' => $timestamp_item->reveal(),
+      'datetime_allowed_formats' => [\DateTime::RFC3339],
+    ];
 
-    return $data;
-  }
-
-  /**
-   * Tests the denormalize function with bad data.
-   *
-   * @covers ::denormalize
-   */
-  public function testDenormalizeException() {
-    $this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06 09:02am GMT" is not in an accepted format: "U" (UNIX timestamp), "Y-m-d\TH:i:sO" (ISO 8601), "Y-m-d\TH:i:sP" (RFC 3339).');
+    // Mock Serializer service, to assert that the Timestamp @DataType
+    // denormalizer would be called.
+    $serializer_prophecy = $this->prophesize(Serializer::class);
+    // This is where \Drupal\serialization\Normalizer\TimestampNormalizer would
+    // be called.
+    $serializer_prophecy->denormalize($timestamp_item_normalization['value'], Timestamp::class, NULL, $context)
+      ->willReturn($timestamp_data_denormalization)
+      ->shouldBeCalled();
 
-    $context = ['target_instance' => $this->createTimestampItemProphecy()->reveal()];
+    $this->normalizer->setSerializer($serializer_prophecy->reveal());
 
-    $normalized = ['value' => '2016/11/06 09:02am GMT'];
-    $this->normalizer->denormalize($normalized, TimestampItem::class, NULL, $context);
+    $denormalized = $this->normalizer->denormalize($timestamp_item_normalization, TimestampItem::class, NULL, $context);
+    $this->assertTrue($denormalized instanceof TimestampItem);
   }
 
   /**
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/TimestampNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampNormalizerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e7ddb8656ac6975aee2ebf1c2704802e0c0a6954
--- /dev/null
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/TimestampNormalizerTest.php
@@ -0,0 +1,147 @@
+<?php
+
+namespace Drupal\Tests\serialization\Unit\Normalizer;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\TypedData\Plugin\DataType\IntegerData;
+use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
+use Drupal\Core\TypedData\Type\DateTimeInterface;
+use Drupal\serialization\Normalizer\TimestampNormalizer;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
+/**
+ * Unit test coverage for the "Timestamp" @DataType.
+ *
+ * @group serialization
+ * @coversDefaultClass \Drupal\serialization\Normalizer\TimestampNormalizer
+ * @see \Drupal\Core\TypedData\Plugin\DataType\Timestamp
+ */
+class TimestampNormalizerTest extends UnitTestCase {
+
+  /**
+   * The tested data type's normalizer.
+   *
+   * @var \Drupal\serialization\Normalizer\TimestampNormalizer
+   */
+  protected $normalizer;
+
+  /**
+   * The tested data type.
+   *
+   * @var \Drupal\Core\Field\Plugin\Field\FieldType\TimestampItem
+   */
+  protected $data;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->normalizer = new TimestampNormalizer($this->prophesize(ConfigFactoryInterface::class)->reveal());
+    $this->data = $this->prophesize(Timestamp::class);
+  }
+
+  /**
+   * @covers ::supportsNormalization
+   */
+  public function testSupportsNormalization() {
+    $this->assertTrue($this->normalizer->supportsNormalization($this->data->reveal()));
+
+    $integer = $this->prophesize(IntegerData::class);
+    $this->assertFalse($this->normalizer->supportsNormalization($integer->reveal()));
+
+    $datetime = $this->prophesize(DateTimeInterface::class);
+    $this->assertFalse($this->normalizer->supportsNormalization($datetime->reveal()));
+  }
+
+  /**
+   * @covers ::supportsDenormalization
+   */
+  public function testSupportsDenormalization() {
+    $this->assertTrue($this->normalizer->supportsDenormalization($this->data->reveal(), Timestamp::class));
+  }
+
+  /**
+   * @covers ::normalize
+   */
+  public function testNormalize() {
+    $random_rfc_3339_string = $this->randomMachineName();
+
+    $drupal_date_time = $this->prophesize(TimestampNormalizerTestDrupalDateTime::class);
+    $drupal_date_time->setTimezone(new \DateTimeZone('UTC'))
+      ->willReturn($drupal_date_time->reveal());
+    $drupal_date_time->format(\DateTime::RFC3339)
+      ->willReturn($random_rfc_3339_string);
+
+    $this->data->getDateTime()
+      ->willReturn($drupal_date_time->reveal());
+
+    $normalized = $this->normalizer->normalize($this->data->reveal());
+    $this->assertSame($random_rfc_3339_string, $normalized);
+  }
+
+  /**
+   * Tests the denormalize function with good data.
+   *
+   * @covers ::denormalize
+   * @dataProvider providerTestDenormalizeValidFormats
+   */
+  public function testDenormalizeValidFormats($normalized, $expected) {
+    $denormalized = $this->normalizer->denormalize($normalized, Timestamp::class, NULL, []);
+    $this->assertSame($expected, $denormalized);
+  }
+
+  /**
+   * Data provider for testDenormalizeValidFormats.
+   *
+   * @return array
+   */
+  public function providerTestDenormalizeValidFormats() {
+    $expected_stamp = 1478422920;
+
+    $data = [];
+
+    $data['U'] = [$expected_stamp, $expected_stamp];
+    $data['RFC3339'] = ['2016-11-06T09:02:00+00:00', $expected_stamp];
+    $data['RFC3339 +0100'] = ['2016-11-06T09:02:00+01:00', $expected_stamp - 1 * 3600];
+    $data['RFC3339 -0600'] = ['2016-11-06T09:02:00-06:00', $expected_stamp + 6 * 3600];
+
+    $data['ISO8601'] = ['2016-11-06T09:02:00+0000', $expected_stamp];
+    $data['ISO8601 +0100'] = ['2016-11-06T09:02:00+0100', $expected_stamp - 1 * 3600];
+    $data['ISO8601 -0600'] = ['2016-11-06T09:02:00-0600', $expected_stamp + 6 * 3600];
+
+    return $data;
+  }
+
+  /**
+   * Tests the denormalize function with bad data.
+   *
+   * @covers ::denormalize
+   */
+  public function testDenormalizeException() {
+    $this->setExpectedException(UnexpectedValueException::class, 'The specified date "2016/11/06 09:02am GMT" is not in an accepted format: "U" (UNIX timestamp), "Y-m-d\TH:i:sO" (ISO 8601), "Y-m-d\TH:i:sP" (RFC 3339).');
+
+    $normalized = '2016/11/06 09:02am GMT';
+
+    $this->normalizer->denormalize($normalized, Timestamp::class, NULL, []);
+  }
+
+}
+
+/**
+ * Note: Prophecy does not support magic methods. By subclassing and specifying
+ * an explicit method, Prophecy works.
+ * @see https://github.com/phpspec/prophecy/issues/338
+ * @see https://github.com/phpspec/prophecy/issues/34
+ * @see https://github.com/phpspec/prophecy/issues/80
+ */
+class TimestampNormalizerTestDrupalDateTime extends DrupalDateTime {
+
+  public function setTimezone(\DateTimeZone $timezone) {
+    parent::setTimezone($timezone);
+  }
+
+}