 jsonapi.services.yml                               |  21 ++-
 src/Controller/EntityResource.php                  |  30 +---
 .../Normalizer/DateTimeIso8601Normalizer.php       |  65 +++++++++
 .../Normalizer/DateTimeNormalizer.php              |  97 +++++++++++++
 .../Normalizer/TimestampNormalizer.php             |  55 ++++++++
 src/Normalizer/ConfigEntityNormalizer.php          |   8 ++
 src/Normalizer/EntityNormalizer.php                |  57 +++++++-
 src/Normalizer/EntityReferenceFieldNormalizer.php  | 154 +--------------------
 src/Normalizer/FieldItemNormalizer.php             |  31 ++++-
 src/Normalizer/FieldNormalizer.php                 |  33 ++++-
 src/Normalizer/RelationshipNormalizer.php          | 151 +++++++++++++++++++-
 src/Routing/Routes.php                             |   4 +-
 .../src/Normalizer/StringNormalizer.php            |  10 +-
 .../src/Normalizer/StringNormalizer.php            |  12 +-
 tests/src/Functional/BlockContentTest.php          |  14 +-
 tests/src/Functional/CommentTest.php               |  10 +-
 tests/src/Functional/EntityTestTest.php            |   4 +-
 tests/src/Functional/ExternalNormalizersTest.php   |  57 +++++++-
 tests/src/Functional/FeedTest.php                  |  13 +-
 tests/src/Functional/FileTest.php                  |   8 +-
 tests/src/Functional/MediaTest.php                 |  15 +-
 tests/src/Functional/MenuLinkContentTest.php       |   7 +-
 tests/src/Functional/NodeTest.php                  |  15 +-
 tests/src/Functional/TermTest.php                  |   7 +-
 tests/src/Functional/UserTest.php                  |  11 +-
 .../JsonApiDocumentTopLevelNormalizerTest.php      |   1 -
 .../Unit/Normalizer/ConfigEntityNormalizerTest.php |   6 +-
 ...izerTest.php => RelationshipNormalizerTest.php} |  10 +-
 tests/src/Unit/Routing/RoutesTest.php              |   3 +-
 29 files changed, 614 insertions(+), 295 deletions(-)

diff --git a/jsonapi.services.yml b/jsonapi.services.yml
index 97a68a6..7bed88a 100644
--- a/jsonapi.services.yml
+++ b/jsonapi.services.yml
@@ -59,17 +59,17 @@ services:
       - { name: jsonapi_normalizer_do_not_use_removal_imminent }
   serializer.normalizer.relationship.jsonapi:
     class: Drupal\jsonapi\Normalizer\RelationshipNormalizer
-    arguments: ['@jsonapi.resource_type.repository', '@jsonapi.link_manager']
+    arguments: ['@jsonapi.resource_type.repository', '@jsonapi.link_manager', '@entity_field.manager', '@plugin.manager.field.field_type', '@entity.repository']
     tags:
       - { name: jsonapi_normalizer_do_not_use_removal_imminent }
   serializer.normalizer.entity.jsonapi:
     class: Drupal\jsonapi\Normalizer\ContentEntityNormalizer
-    arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager']
+    arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager', '@entity_field.manager', '@plugin.manager.field.field_type']
     tags:
       - { name: jsonapi_normalizer_do_not_use_removal_imminent }
   serializer.normalizer.config_entity.jsonapi:
     class: Drupal\jsonapi\Normalizer\ConfigEntityNormalizer
-    arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager']
+    arguments: ['@jsonapi.link_manager', '@jsonapi.resource_type.repository', '@entity_type.manager', '@entity_field.manager', '@plugin.manager.field.field_type']
     tags:
       - { name: jsonapi_normalizer_do_not_use_removal_imminent }
   serializer.normalizer.jsonapi_document_toplevel.jsonapi:
@@ -79,7 +79,7 @@ services:
       - { name: jsonapi_normalizer_do_not_use_removal_imminent }
   serializer.normalizer.entity_reference_field.jsonapi:
     class: Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer
-    arguments: ['@jsonapi.link_manager', '@entity_field.manager', '@plugin.manager.field.field_type', '@jsonapi.resource_type.repository', '@entity.repository']
+    arguments: ['@jsonapi.resource_type.repository', '@entity.repository']
     tags:
       # This must have a higher priority than the 'serializer.normalizer.field.jsonapi' to take effect.
       - { name: jsonapi_normalizer_do_not_use_removal_imminent, priority: 1 }
@@ -171,3 +171,16 @@ services:
   serializer.normalizer.htt_exception.jsonapi:
     alias: serializer.normalizer.http_exception.jsonapi
     deprecated: The "%service_id%" service is deprecated. You should use the 'serializer.normalizer.http_exception.jsonapi' service instead.
+
+  # Forward compatibility.
+  # @todo Remove in Drupal 8.6 (assuming it contains https://www.drupal.org/project/drupal/issues/2926508).
+  serializer.normalizer.timestamp.jsonapi:
+    class: \Drupal\jsonapi\ForwardCompatibility\Normalizer\TimestampNormalizer
+    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.jsonapi:
+    class: \Drupal\jsonapi\ForwardCompatibility\Normalizer\DateTimeIso8601Normalizer
+    tags:
+      # Priority must be higher than serializer.normalizer.primitive_data.
+      - { name: normalizer, priority: 20 }
diff --git a/src/Controller/EntityResource.php b/src/Controller/EntityResource.php
index 82db627..abdd0a8 100644
--- a/src/Controller/EntityResource.php
+++ b/src/Controller/EntityResource.php
@@ -32,7 +32,6 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
 use Symfony\Component\HttpKernel\Exception\HttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
 
 /**
  * Process all entity requests.
@@ -196,18 +195,9 @@ class EntityResource {
       $received_attributes = array_keys($document['data']['attributes']);
       foreach ($received_attributes as $field_name) {
         $internal_field_name = $this->resourceType->getInternalName($field_name);
-        try {
-          $field_access = $entity->get($internal_field_name)->access('edit', NULL, TRUE);
-          if (!$field_access->isAllowed()) {
-            throw new EntityAccessDeniedHttpException(NULL, $field_access, '/data/attributes/' . $field_name, sprintf('The current user is not allowed to POST the selected field (%s).', $field_name));
-          }
-        }
-        catch (\InvalidArgumentException $e) {
-          throw new UnprocessableEntityHttpException(sprintf(
-            'The attribute %s does not exist on the %s resource type.',
-            $internal_field_name,
-            $this->resourceType->getTypeName()
-          ));
+        $field_access = $entity->get($internal_field_name)->access('edit', NULL, TRUE);
+        if (!$field_access->isAllowed()) {
+          throw new EntityAccessDeniedHttpException(NULL, $field_access, '/data/attributes/' . $field_name, sprintf('The current user is not allowed to POST the selected field (%s).', $field_name));
         }
       }
     }
@@ -840,18 +830,8 @@ class EntityResource {
     // The update is different for configuration entities and content entities.
     if ($origin instanceof ContentEntityInterface && $destination instanceof ContentEntityInterface) {
       // First scenario: both are content entities.
-      try {
-        $field_name = $this->resourceType->getInternalName($field_name);
-        $destination_field_list = $destination->get($field_name);
-      }
-      catch (\InvalidArgumentException $e) {
-        $resource_type = $this->resourceTypeRepository->get($destination->getEntityTypeId(), $destination->bundle());
-        throw new UnprocessableEntityHttpException(sprintf(
-          'The attribute %s does not exist on the %s resource type.',
-          $field_name,
-          $resource_type->getTypeName()
-        ));
-      }
+      $field_name = $this->resourceType->getInternalName($field_name);
+      $destination_field_list = $destination->get($field_name);
 
       $origin_field_list = $origin->get($field_name);
       if ($this->checkPatchFieldAccess($destination_field_list, $origin_field_list)) {
diff --git a/src/ForwardCompatibility/Normalizer/DateTimeIso8601Normalizer.php b/src/ForwardCompatibility/Normalizer/DateTimeIso8601Normalizer.php
new file mode 100644
index 0000000..6a5a187
--- /dev/null
+++ b/src/ForwardCompatibility/Normalizer/DateTimeIso8601Normalizer.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\jsonapi\ForwardCompatibility\Normalizer;
+
+use Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
+
+/**
+ * Converts values for the DateTimeIso8601 data type to RFC3339.
+ *
+ * @internal
+ * @see \Drupal\serialization\Normalizer\DateTimeIso8601Normalizer
+ * @todo Remove when JSON API requires Drupal 8.6.
+ */
+class DateTimeIso8601Normalizer extends DateTimeNormalizer {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $allowedFormats = [
+    // 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 = []) {
+    $field_item = $datetime->getParent();
+    if ($field_item instanceof DateTimeItem && $field_item->getFieldDefinition()->getFieldStorageDefinition()->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) {
+      return $datetime->getDateTime()->format($this->allowedFormats['date-only']);
+    }
+    return parent::normalize($datetime, $format, $context);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    $date = parent::denormalize($data, $class, $format, $context);
+    // Extract the year, month, and day from the object.
+    $ymd = $date->format('Y-m-d');
+    // Rebuild the date object using the extracted year, month, and day, but
+    // for consistency set the time to 12:00:00 UTC upon creation for date-only
+    // fields. Rebuilding, instead of using the object methods, is done to
+    // avoid the initial date object picking up the local time and time zone
+    // from an input value with a missing or partial time string, and then
+    // rolling over to a different day when changing the object to UTC.
+    // @see \Drupal\Component\Datetime\DateTimePlus::setDefaultDateTime()
+    // @see \Drupal\datetime\Plugin\views\filter\Date::getOffset()
+    // @see \Drupal\datetime\DateTimeComputed::getValue()
+    // @see http://php.net/manual/en/datetime.createfromformat.php
+    return \DateTime::createFromFormat('Y-m-d\TH:i:s e', $ymd . 'T12:00:00 UTC');
+  }
+
+}
diff --git a/src/ForwardCompatibility/Normalizer/DateTimeNormalizer.php b/src/ForwardCompatibility/Normalizer/DateTimeNormalizer.php
new file mode 100644
index 0000000..f04081e
--- /dev/null
+++ b/src/ForwardCompatibility/Normalizer/DateTimeNormalizer.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace Drupal\jsonapi\ForwardCompatibility\Normalizer;
+
+use Drupal\Core\TypedData\Type\DateTimeInterface;
+use Drupal\jsonapi\Normalizer\NormalizerBase;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+/**
+ * Converts values for datetime objects to RFC3339 and from common formats.
+ *
+ * @internal
+ * @see \Drupal\serialization\Normalizer\DateTimeNormalizer
+ * @todo Remove when JSON API requires Drupal 8.6.
+ */
+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;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($datetime, $format = NULL, array $context = []) {
+    return $datetime->getDateTime()
+      // 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 = \Drupal::config('system.date')->get('timezone.default');
+    return new \DateTimeZone($default_site_timezone);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    // First check for a provided format, and if provided, create \DateTime
+    // object using it.
+    if (!empty($context['datetime_format'])) {
+      return \DateTime::createFromFormat($context['datetime_format'], $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.
+    foreach ($this->allowedFormats as $format) {
+      $date = \DateTime::createFromFormat($format, $data);
+      if ($date !== FALSE) {
+        return $date;
+      }
+    }
+
+    $format_strings = [];
+
+    foreach ($this->allowedFormats 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/src/ForwardCompatibility/Normalizer/TimestampNormalizer.php b/src/ForwardCompatibility/Normalizer/TimestampNormalizer.php
new file mode 100644
index 0000000..1a00d87
--- /dev/null
+++ b/src/ForwardCompatibility/Normalizer/TimestampNormalizer.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\jsonapi\ForwardCompatibility\Normalizer;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\TypedData\Plugin\DataType\Timestamp;
+
+/**
+ * Converts values for the Timestamp data type to and from common formats.
+ *
+ * @internal
+ * @see \Drupal\serialization\Normalizer\TimestampNormalizer
+ * @todo Remove when JSON API requires Drupal 8.6.
+ */
+class TimestampNormalizer extends DateTimeNormalizer {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $allowedFormats = [
+    'UNIX timestamp' => 'U',
+    'ISO 8601' => \DateTime::ISO8601,
+    'RFC 3339' => \DateTime::RFC3339,
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = Timestamp::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($datetime, $format = NULL, array $context = []) {
+    return DrupalDateTime::createFromTimestamp($datetime->getValue())
+      ->setTimezone($this->getNormalizationTimezone())
+      ->format(\DateTime::RFC3339);
+  }
+
+  /**
+   * {@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/src/Normalizer/ConfigEntityNormalizer.php b/src/Normalizer/ConfigEntityNormalizer.php
index 0345fa3..1e4e59c 100644
--- a/src/Normalizer/ConfigEntityNormalizer.php
+++ b/src/Normalizer/ConfigEntityNormalizer.php
@@ -79,4 +79,12 @@ class ConfigEntityNormalizer extends EntityNormalizer {
     return array_diff_key($data, ['_core' => TRUE]);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareInput(array $data, ResourceType $resource_type, $format, array $context) {
+    // Config entities' data don't have properties needing denormalization.
+    return $data;
+  }
+
 }
diff --git a/src/Normalizer/EntityNormalizer.php b/src/Normalizer/EntityNormalizer.php
index 7f62006..506ed9c 100644
--- a/src/Normalizer/EntityNormalizer.php
+++ b/src/Normalizer/EntityNormalizer.php
@@ -3,7 +3,9 @@
 namespace Drupal\jsonapi\Normalizer;
 
 use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
 use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
 use Drupal\jsonapi\Normalizer\Value\EntityNormalizerValue;
 use Drupal\jsonapi\Normalizer\Value\FieldNormalizerValueInterface;
@@ -11,6 +13,7 @@ use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\LinkManager\LinkManager;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
 use Symfony\Component\HttpKernel\Exception\PreconditionFailedHttpException;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
 use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
@@ -56,6 +59,20 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
   protected $entityTypeManager;
 
   /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $fieldManager;
+
+  /**
+   * The field plugin manager.
+   *
+   * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
    * Constructs an EntityNormalizer object.
    *
    * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager
@@ -64,11 +81,17 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
    *   The JSON API resource type repository.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity type manager.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
+   *   The entity field manager.
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $plugin_manager
+   *   The plugin manager for fields.
    */
-  public function __construct(LinkManager $link_manager, ResourceTypeRepositoryInterface $resource_type_repository, EntityTypeManagerInterface $entity_type_manager) {
+  public function __construct(LinkManager $link_manager, ResourceTypeRepositoryInterface $resource_type_repository, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $field_manager, FieldTypePluginManagerInterface $plugin_manager) {
     $this->linkManager = $link_manager;
     $this->resourceTypeRepository = $resource_type_repository;
     $this->entityTypeManager = $entity_type_manager;
+    $this->fieldManager = $field_manager;
+    $this->pluginManager = $plugin_manager;
   }
 
   /**
@@ -123,7 +146,7 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
     }
 
     return $this->entityTypeManager->getStorage($entity_type_id)
-      ->create($this->prepareInput($data, $resource_type));
+      ->create($this->prepareInput($data, $resource_type, $format, $context));
   }
 
   /**
@@ -205,12 +228,19 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
    *   The input data to modify.
    * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
    *   Contains the info about the resource type.
+   * @param string $format
+   *   Format the given data was extracted from.
+   * @param array $context
+   *   Options available to the denormalizer.
    *
    * @return array
    *   The modified input data.
    */
-  protected function prepareInput(array $data, ResourceType $resource_type) {
+  protected function prepareInput(array $data, ResourceType $resource_type, $format, array $context) {
     $data_internal = [];
+
+    $field_map = $this->fieldManager->getFieldMap()[$resource_type->getEntityTypeId()];
+
     // Translate the public fields into the entity fields.
     foreach ($data as $public_field_name => $field_value) {
       // Skip any disabled field.
@@ -218,7 +248,26 @@ class EntityNormalizer extends NormalizerBase implements DenormalizerInterface {
         continue;
       }
       $internal_name = $resource_type->getInternalName($public_field_name);
-      $data_internal[$internal_name] = $field_value;
+
+      if (!isset($field_map[$internal_name]) || !in_array($resource_type->getBundle(), $field_map[$internal_name]['bundles'], TRUE)) {
+        throw new UnprocessableEntityHttpException(sprintf(
+          'The attribute %s does not exist on the %s resource type.',
+          $internal_name,
+          $resource_type->getTypeName()
+        ));
+      }
+
+      $field_type = $field_map[$internal_name]['type'];
+      $field_class = $this->pluginManager->getDefinition($field_type)['list_class'];
+
+      $context['field_type'] = $field_type;
+      $context['field_name'] = $internal_name;
+      $context['field_definition'] = $this->fieldManager->getFieldDefinitions($resource_type->getEntityTypeId(), $resource_type->getBundle())[$internal_name];
+      // @todo use \Drupal\Core\Field\FieldTypePluginManagerInterface::createFieldItemList()
+      $data_internal[$internal_name] = $this->serializer->denormalize($field_value, $field_class, $format, $context);
+      unset($context['field_type']);
+      unset($context['field_name']);
+      unset($context['field_definition']);
     }
 
     return $data_internal;
diff --git a/src/Normalizer/EntityReferenceFieldNormalizer.php b/src/Normalizer/EntityReferenceFieldNormalizer.php
index 7d90ca8..6d71349 100644
--- a/src/Normalizer/EntityReferenceFieldNormalizer.php
+++ b/src/Normalizer/EntityReferenceFieldNormalizer.php
@@ -2,26 +2,20 @@
 
 namespace Drupal\jsonapi\Normalizer;
 
-use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
-use Drupal\Core\Field\FieldTypePluginManagerInterface;
-use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
 use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
 use Drupal\jsonapi\Normalizer\Value\NullFieldNormalizerValue;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
 use Drupal\jsonapi\Resource\EntityCollection;
-use Drupal\jsonapi\LinkManager\LinkManager;
-use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
-use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
 /**
  * Normalizer class specific for entity reference field objects.
  *
  * @internal
  */
-class EntityReferenceFieldNormalizer extends FieldNormalizer implements DenormalizerInterface {
+class EntityReferenceFieldNormalizer extends FieldNormalizer {
 
   /**
    * {@inheritdoc}
@@ -29,27 +23,6 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
   protected $supportedInterfaceOrClass = EntityReferenceFieldItemListInterface::class;
 
   /**
-   * The link manager.
-   *
-   * @var \Drupal\jsonapi\LinkManager\LinkManager
-   */
-  protected $linkManager;
-
-  /**
-   * The entity field manager.
-   *
-   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
-   */
-  protected $fieldManager;
-
-  /**
-   * The field plugin manager.
-   *
-   * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
-   */
-  protected $pluginManager;
-
-  /**
    * The entity repository.
    *
    * @var \Drupal\Core\Entity\EntityRepositoryInterface
@@ -59,21 +32,12 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
   /**
    * Instantiates a EntityReferenceFieldNormalizer object.
    *
-   * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager
-   *   The link manager.
-   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
-   *   The entity field manager.
-   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $plugin_manager
-   *   The plugin manager for fields.
    * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
    *   The JSON API resource type repository.
    * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
    *   The entity repository.
    */
-  public function __construct(LinkManager $link_manager, EntityFieldManagerInterface $field_manager, FieldTypePluginManagerInterface $plugin_manager, ResourceTypeRepositoryInterface $resource_type_repository, EntityRepositoryInterface $entity_repository) {
-    $this->linkManager = $link_manager;
-    $this->fieldManager = $field_manager;
-    $this->pluginManager = $plugin_manager;
+  public function __construct(ResourceTypeRepositoryInterface $resource_type_repository, EntityRepositoryInterface $entity_repository) {
     $this->resourceTypeRepository = $resource_type_repository;
     $this->entityRepository = $entity_repository;
   }
@@ -133,97 +97,6 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
   }
 
   /**
-   * {@inheritdoc}
-   */
-  public function denormalize($data, $class, $format = NULL, array $context = []) {
-    // If we get to here is through a write method on a relationship operation.
-    /** @var \Drupal\jsonapi\ResourceType\ResourceType $resource_type */
-    $resource_type = $context['resource_type'];
-    $entity_type_id = $resource_type->getEntityTypeId();
-    $field_definitions = $this->fieldManager->getFieldDefinitions(
-      $entity_type_id,
-      $resource_type->getBundle()
-    );
-    if (empty($context['related']) || empty($field_definitions[$context['related']])) {
-      throw new BadRequestHttpException('Invalid or missing related field.');
-    }
-    /* @var \Drupal\field\Entity\FieldConfig $field_definition */
-    $field_definition = $field_definitions[$context['related']];
-    // This is typically 'target_id'.
-    $item_definition = $field_definition->getItemDefinition();
-    $property_key = $item_definition->getMainPropertyName();
-    $target_resources = $this->getAllowedResourceTypes($item_definition);
-
-    $is_multiple = $field_definition->getFieldStorageDefinition()->isMultiple();
-    $data = $this->massageRelationshipInput($data, $is_multiple);
-    $values = array_map(function ($value) use ($property_key, $target_resources) {
-      // Make sure that the provided type is compatible with the targeted
-      // resource.
-      if (!in_array($value['type'], $target_resources)) {
-        throw new BadRequestHttpException(sprintf(
-          'The provided type (%s) does not mach the destination resource types (%s).',
-          $value['type'],
-          implode(', ', $target_resources)
-        ));
-      }
-
-      // Load the entity by UUID.
-      list($entity_type_id,) = explode('--', $value['type']);
-      $entity = $this->entityRepository->loadEntityByUuid($entity_type_id, $value['id']);
-      $value['id'] = $entity ? $entity->id() : NULL;
-
-      $properties = [$property_key => $value['id']];
-      // Also take into account additional properties provided by the field
-      // type.
-      if (!empty($value['meta'])) {
-        foreach ($value['meta'] as $meta_key => $meta_value) {
-          $properties[$meta_key] = $meta_value;
-        }
-      }
-      return $properties;
-    }, $data['data']);
-    return $this->pluginManager
-      ->createFieldItemList($context['target_entity'], $context['related'], $values);
-  }
-
-  /**
-   * Validates and massages the relationship input depending on the cardinality.
-   *
-   * @param array $data
-   *   The input data from the body.
-   * @param bool $is_multiple
-   *   Indicates if the relationship is to-many.
-   *
-   * @return array
-   *   The massaged data array.
-   */
-  protected function massageRelationshipInput(array $data, $is_multiple) {
-    if ($is_multiple) {
-      if (!is_array($data['data'])) {
-        throw new BadRequestHttpException('Invalid body payload for the relationship.');
-      }
-      // Leave the invalid elements.
-      $invalid_elements = array_filter($data['data'], function ($element) {
-        return empty($element['type']) || empty($element['id']);
-      });
-      if ($invalid_elements) {
-        throw new BadRequestHttpException('Invalid body payload for the relationship.');
-      }
-    }
-    else {
-      // For to-one relationships you can have a NULL value.
-      if (is_null($data['data'])) {
-        return ['data' => []];
-      }
-      if (empty($data['data']['type']) || empty($data['data']['id'])) {
-        throw new BadRequestHttpException('Invalid body payload for the relationship.');
-      }
-      $data['data'] = [$data['data']];
-    }
-    return $data;
-  }
-
-  /**
    * Determines if the given entity is of an internal resource type.
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
@@ -239,27 +112,4 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal
     )) && $resource_type->isInternal();
   }
 
-  /**
-   * Build the list of resource types supported by this entity reference field.
-   *
-   * @param \Drupal\Core\Field\TypedData\FieldItemDataDefinition $item_definition
-   *   The field item definition.
-   *
-   * @return string[]
-   *   List of resource types.
-   */
-  protected function getAllowedResourceTypes(FieldItemDataDefinition $item_definition) {
-    // Build the list of allowed resources.
-    $target_entity_id = $item_definition->getSetting('target_type');
-    $handler_settings = $item_definition->getSetting('handler_settings');
-    $target_bundles = empty($handler_settings['target_bundles']) ?
-      [] :
-      $handler_settings['target_bundles'];
-    return array_map(function ($target_bundle) use ($target_entity_id) {
-      return $this->resourceTypeRepository
-        ->get($target_entity_id, $target_bundle)
-        ->getTypeName();
-    }, $target_bundles);
-  }
-
 }
diff --git a/src/Normalizer/FieldItemNormalizer.php b/src/Normalizer/FieldItemNormalizer.php
index 388741a..0a42fe2 100644
--- a/src/Normalizer/FieldItemNormalizer.php
+++ b/src/Normalizer/FieldItemNormalizer.php
@@ -4,16 +4,17 @@ namespace Drupal\jsonapi\Normalizer;
 
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Field\FieldItemInterface;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
 use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
 use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
-use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
  * Converts the Drupal field item object to a JSON API array structure.
  *
  * @internal
  */
-class FieldItemNormalizer extends NormalizerBase {
+class FieldItemNormalizer extends NormalizerBase implements DenormalizerInterface {
 
   /**
    * The interface or class that this Normalizer supports.
@@ -67,7 +68,31 @@ class FieldItemNormalizer extends NormalizerBase {
    * {@inheritdoc}
    */
   public function denormalize($data, $class, $format = NULL, array $context = []) {
-    throw new UnexpectedValueException('Denormalization not implemented for JSON API');
+    $item_definition = $context['field_definition']->getItemDefinition();
+    assert($item_definition instanceof FieldItemDataDefinitionInterface);
+
+    $property_definitions = $item_definition->getPropertyDefinitions();
+
+    // Because e.g. the 'bundle' entity key field requires field values to not
+    // be expanded to an array of all properties, we special-case single-value
+    // properties.
+    if (!is_array($data)) {
+      $property_value = $data;
+      $property_value_class = $property_definitions[$item_definition->getMainPropertyName()]->getClass();
+      return $this->serializer->supportsDenormalization($property_value, $property_value_class, $format, $context)
+        ? $this->serializer->denormalize($property_value, $property_value_class, $format, $context)
+        : $property_value;
+    }
+
+    $data_internal = [];
+    foreach ($data as $property_name => $property_value) {
+      $property_value_class = $property_definitions[$property_name]->getClass();
+      $data_internal[$property_name] = $this->serializer->supportsDenormalization($property_value, $property_value_class, $format, $context)
+        ? $this->serializer->denormalize($property_value, $property_value_class, $format, $context)
+        : $property_value;
+    }
+
+    return $data_internal;
   }
 
 }
diff --git a/src/Normalizer/FieldNormalizer.php b/src/Normalizer/FieldNormalizer.php
index fd354d8..6b3709d 100644
--- a/src/Normalizer/FieldNormalizer.php
+++ b/src/Normalizer/FieldNormalizer.php
@@ -4,18 +4,19 @@ namespace Drupal\jsonapi\Normalizer;
 
 use Drupal\Component\Assertion\Inspector;
 use Drupal\Core\Field\EntityReferenceFieldItemList;
+use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\jsonapi\Normalizer\Value\FieldItemNormalizerValue;
 use Drupal\jsonapi\Normalizer\Value\FieldNormalizerValue;
 use Drupal\jsonapi\Normalizer\Value\NullFieldNormalizerValue;
-use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
  * Converts the Drupal field structure to a JSON API array structure.
  *
  * @internal
  */
-class FieldNormalizer extends NormalizerBase {
+class FieldNormalizer extends NormalizerBase implements DenormalizerInterface {
 
   /**
    * The interface or class that this Normalizer supports.
@@ -73,7 +74,33 @@ class FieldNormalizer extends NormalizerBase {
    * {@inheritdoc}
    */
   public function denormalize($data, $class, $format = NULL, array $context = []) {
-    throw new UnexpectedValueException('Denormalization not implemented for JSON API');
+    $field_definition = $context['field_definition'];
+    assert($field_definition instanceof FieldDefinitionInterface);
+
+    // If $data contains items (recognizable by numerical array keys, which
+    // Drupal's Field API calls "deltas"), then it already is itemized; it's not
+    // using the simplified JSON structure that JSON API generates.
+    $is_already_itemized = is_array($data) && array_reduce(array_keys($data), function ($carry, $index) {
+      return $carry && is_numeric($index);
+    }, TRUE);
+
+    $itemized_data = $is_already_itemized
+      ? $data
+      : [0 => $data];
+
+    // Single-cardinality fields don't need itemization.
+    $field_item_class = $field_definition->getItemDefinition()->getClass();
+    if (count($itemized_data) === 1 && $field_definition->getFieldStorageDefinition()->getCardinality() === 1) {
+      return $this->serializer->denormalize($itemized_data[0], $field_item_class, $format, $context);
+    }
+
+    $data_internal = [];
+    foreach ($itemized_data as $delta => $field_item_value) {
+      // @todo use \Drupal\Core\Field\FieldTypePluginManagerInterface::createFieldItem()
+      $data_internal[$delta] = $this->serializer->denormalize($field_item_value, $field_item_class, $format, $context);
+    }
+
+    return $data_internal;
   }
 
   /**
diff --git a/src/Normalizer/RelationshipNormalizer.php b/src/Normalizer/RelationshipNormalizer.php
index 8698c4e..c1633cc 100644
--- a/src/Normalizer/RelationshipNormalizer.php
+++ b/src/Normalizer/RelationshipNormalizer.php
@@ -3,11 +3,16 @@
 namespace Drupal\jsonapi\Normalizer;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
 use Drupal\jsonapi\Normalizer\Value\RelationshipNormalizerValue;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
 use Drupal\jsonapi\LinkManager\LinkManager;
-use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
  * Normalizes a Relationship according to the JSON API specification.
@@ -17,7 +22,7 @@ use Symfony\Component\Serializer\Exception\UnexpectedValueException;
  *
  * @internal
  */
-class RelationshipNormalizer extends NormalizerBase {
+class RelationshipNormalizer extends NormalizerBase implements DenormalizerInterface {
 
   /**
    * The interface or class that this Normalizer supports.
@@ -41,23 +46,161 @@ class RelationshipNormalizer extends NormalizerBase {
   protected $linkManager;
 
   /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $fieldManager;
+
+  /**
+   * The field plugin manager.
+   *
+   * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * The entity repository.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
+  /**
    * RelationshipNormalizer constructor.
    *
    * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
    *   The JSON API resource type repository.
    * @param \Drupal\jsonapi\LinkManager\LinkManager $link_manager
    *   The link manager.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
+   *   The entity field manager.
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $plugin_manager
+   *   The plugin manager for fields.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
+   *   The entity repository.
    */
-  public function __construct(ResourceTypeRepositoryInterface $resource_type_repository, LinkManager $link_manager) {
+  public function __construct(ResourceTypeRepositoryInterface $resource_type_repository, LinkManager $link_manager, EntityFieldManagerInterface $field_manager, FieldTypePluginManagerInterface $plugin_manager, EntityRepositoryInterface $entity_repository) {
     $this->resourceTypeRepository = $resource_type_repository;
     $this->linkManager = $link_manager;
+    $this->fieldManager = $field_manager;
+    $this->resourceTypeRepository = $resource_type_repository;
+    $this->pluginManager = $plugin_manager;
+    $this->entityRepository = $entity_repository;
   }
 
   /**
    * {@inheritdoc}
    */
   public function denormalize($data, $class, $format = NULL, array $context = []) {
-    throw new UnexpectedValueException('Denormalization not implemented for JSON API');
+    // If we get to here is through a write method on a relationship operation.
+    /** @var \Drupal\jsonapi\ResourceType\ResourceType $resource_type */
+    $resource_type = $context['resource_type'];
+    $entity_type_id = $resource_type->getEntityTypeId();
+    $field_definitions = $this->fieldManager->getFieldDefinitions(
+      $entity_type_id,
+      $resource_type->getBundle()
+    );
+    if (empty($context['related']) || empty($field_definitions[$context['related']])) {
+      throw new BadRequestHttpException('Invalid or missing related field.');
+    }
+    /* @var \Drupal\field\Entity\FieldConfig $field_definition */
+    $field_definition = $field_definitions[$context['related']];
+    // This is typically 'target_id'.
+    $item_definition = $field_definition->getItemDefinition();
+    $property_key = $item_definition->getMainPropertyName();
+    $target_resources = $this->getAllowedResourceTypes($item_definition);
+
+    $is_multiple = $field_definition->getFieldStorageDefinition()->isMultiple();
+    $data = $this->massageRelationshipInput($data, $is_multiple);
+    $values = array_map(function ($value) use ($property_key, $target_resources) {
+      // Make sure that the provided type is compatible with the targeted
+      // resource.
+      if (!in_array($value['type'], $target_resources)) {
+        throw new BadRequestHttpException(sprintf(
+          'The provided type (%s) does not mach the destination resource types (%s).',
+          $value['type'],
+          implode(', ', $target_resources)
+        ));
+      }
+
+      // Load the entity by UUID.
+      list($entity_type_id,) = explode('--', $value['type']);
+      $entity = $this->entityRepository->loadEntityByUuid($entity_type_id, $value['id']);
+      $value['id'] = $entity ? $entity->id() : NULL;
+
+      $properties = [$property_key => $value['id']];
+      // Also take into account additional properties provided by the field
+      // type.
+      if (!empty($value['meta'])) {
+        foreach ($value['meta'] as $meta_key => $meta_value) {
+          $properties[$meta_key] = $meta_value;
+        }
+      }
+      return $properties;
+    }, $data['data']);
+    return $this->pluginManager
+      ->createFieldItemList($context['target_entity'], $context['related'], $values);
+  }
+
+  /**
+   * Validates and massages the relationship input depending on the cardinality.
+   *
+   * @param array $data
+   *   The input data from the body.
+   * @param bool $is_multiple
+   *   Indicates if the relationship is to-many.
+   *
+   * @return array
+   *   The massaged data array.
+   */
+  protected function massageRelationshipInput(array $data, $is_multiple) {
+    if ($is_multiple) {
+      if (!is_array($data['data'])) {
+        throw new BadRequestHttpException('Invalid body payload for the relationship.');
+      }
+      // Leave the invalid elements.
+      $invalid_elements = array_filter($data['data'], function ($element) {
+        return empty($element['type']) || empty($element['id']);
+      });
+      if ($invalid_elements) {
+        throw new BadRequestHttpException('Invalid body payload for the relationship.');
+      }
+    }
+    else {
+      // For to-one relationships you can have a NULL value.
+      if (is_null($data['data'])) {
+        return ['data' => []];
+      }
+      if (empty($data['data']['type']) || empty($data['data']['id'])) {
+        throw new BadRequestHttpException('Invalid body payload for the relationship.');
+      }
+      $data['data'] = [$data['data']];
+    }
+    return $data;
+  }
+
+  /**
+   * Build the list of resource types supported by this entity reference field.
+   *
+   * @param \Drupal\Core\Field\TypedData\FieldItemDataDefinition $item_definition
+   *   The field item definition.
+   *
+   * @return string[]
+   *   List of resource types.
+   */
+  protected function getAllowedResourceTypes(FieldItemDataDefinition $item_definition) {
+    // Build the list of allowed resources.
+    $target_entity_id = $item_definition->getSetting('target_type');
+    $handler_settings = $item_definition->getSetting('handler_settings');
+    $target_bundles = empty($handler_settings['target_bundles']) ?
+      [] :
+      $handler_settings['target_bundles'];
+    return array_map(function ($target_bundle) use ($target_entity_id) {
+      return $this->resourceTypeRepository
+        ->get($target_entity_id, $target_bundle)
+        ->getTypeName();
+    }, $target_bundles);
   }
 
   /**
diff --git a/src/Routing/Routes.php b/src/Routing/Routes.php
index a76be19..04236b0 100644
--- a/src/Routing/Routes.php
+++ b/src/Routing/Routes.php
@@ -3,8 +3,8 @@
 namespace Drupal\jsonapi\Routing;
 
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\Core\Field\EntityReferenceFieldItemList;
 use Drupal\jsonapi\Controller\EntryPoint;
+use Drupal\jsonapi\Normalizer\Relationship;
 use Drupal\jsonapi\ParamConverter\ResourceTypeConverter;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
@@ -186,7 +186,7 @@ class Routes implements ContainerInjectionInterface {
     $relationship_route->setMethods(['GET', 'POST', 'PATCH', 'DELETE']);
     // @todo: remove the _on_relationship default in https://www.drupal.org/project/jsonapi/issues/2953346.
     $relationship_route->addDefaults(['_on_relationship' => TRUE]);
-    $relationship_route->addDefaults(['serialization_class' => EntityReferenceFieldItemList::class]);
+    $relationship_route->addDefaults(['serialization_class' => Relationship::class]);
     $relationship_route->setRequirement('_csrf_request_header_token', 'TRUE');
     $routes->add(static::getRouteName($resource_type, 'relationship'), $relationship_route);
 
diff --git a/tests/modules/jsonapi_test_data_type/src/Normalizer/StringNormalizer.php b/tests/modules/jsonapi_test_data_type/src/Normalizer/StringNormalizer.php
index abb6610..1c8d881 100644
--- a/tests/modules/jsonapi_test_data_type/src/Normalizer/StringNormalizer.php
+++ b/tests/modules/jsonapi_test_data_type/src/Normalizer/StringNormalizer.php
@@ -4,11 +4,12 @@ namespace Drupal\jsonapi_test_data_type\Normalizer;
 
 use Drupal\Core\TypedData\Plugin\DataType\StringData;
 use Drupal\serialization\Normalizer\NormalizerBase;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
  * Normalizes string data, with a twist: it replaces 'super' with 'NOT'.
  */
-class StringNormalizer extends NormalizerBase {
+class StringNormalizer extends NormalizerBase implements DenormalizerInterface {
 
   /**
    * {@inheritdoc}
@@ -22,4 +23,11 @@ class StringNormalizer extends NormalizerBase {
     return str_replace('super', 'NOT', $object->getValue());
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    return str_replace('NOT', 'super', $data);
+  }
+
 }
diff --git a/tests/modules/jsonapi_test_field_type/src/Normalizer/StringNormalizer.php b/tests/modules/jsonapi_test_field_type/src/Normalizer/StringNormalizer.php
index 0376ca6..98b3d9b 100644
--- a/tests/modules/jsonapi_test_field_type/src/Normalizer/StringNormalizer.php
+++ b/tests/modules/jsonapi_test_field_type/src/Normalizer/StringNormalizer.php
@@ -4,11 +4,12 @@ namespace Drupal\jsonapi_test_field_type\Normalizer;
 
 use Drupal\Core\Field\Plugin\Field\FieldType\StringItem;
 use Drupal\serialization\Normalizer\FieldItemNormalizer;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
  * Normalizes string fields, with a twist: it replaces 'super' with 'NOT'.
  */
-class StringNormalizer extends FieldItemNormalizer {
+class StringNormalizer extends FieldItemNormalizer implements DenormalizerInterface {
 
   /**
    * {@inheritdoc}
@@ -24,4 +25,13 @@ class StringNormalizer extends FieldItemNormalizer {
     return $data;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function constructValue($data, $context) {
+    $data = parent::constructValue($data, $context);
+    $data['value'] = str_replace('NOT', 'super', $data['value']);
+    return $data;
+  }
+
 }
diff --git a/tests/src/Functional/BlockContentTest.php b/tests/src/Functional/BlockContentTest.php
index 4670478..c2d271e 100644
--- a/tests/src/Functional/BlockContentTest.php
+++ b/tests/src/Functional/BlockContentTest.php
@@ -6,7 +6,6 @@ use Drupal\block_content\Entity\BlockContent;
 use Drupal\block_content\Entity\BlockContentType;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Url;
-use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
 
 /**
  * JSON API integration test for the "BlockContent" content entity type.
@@ -15,8 +14,6 @@ use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
  */
 class BlockContentTest extends ResourceTestBase {
 
-  use BcTimestampNormalizerUnixTestTrait;
-
   /**
    * {@inheritdoc}
    */
@@ -35,7 +32,7 @@ class BlockContentTest extends ResourceTestBase {
   /**
    * {@inheritdoc}
    *
-   * @var \Drupal\config_test\ConfigTestInterface
+   * @var \Drupal\block_content\BlockContentInterface
    */
   protected $entity;
 
@@ -123,16 +120,11 @@ class BlockContentTest extends ResourceTestBase {
             'summary' => NULL,
             'processed' => "<p>The name &quot;llama&quot; was adopted by European settlers from native Peruvians.</p>\n",
           ],
-          'changed' => $this->entity->getChangedTime(),
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'changed' => $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()), */
+          'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
           'info' => 'Llama',
           'revision_id' => 1,
           'revision_log' => NULL,
-          'revision_created' => (int) $this->entity->getRevisionCreationTime(),
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'revision_created' => $this->formatExpectedTimestampItemValues($this->entity->getRevisionCreationTime()), */
-          // @todo Attempt to remove this in https://www.drupal.org/project/drupal/issues/2933518.
+          'revision_created' => (new \DateTime())->setTimestamp($this->entity->getRevisionCreationTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
           'revision_translation_affected' => TRUE,
           'status' => FALSE,
           'langcode' => 'en',
diff --git a/tests/src/Functional/CommentTest.php b/tests/src/Functional/CommentTest.php
index b26f477..41d212f 100644
--- a/tests/src/Functional/CommentTest.php
+++ b/tests/src/Functional/CommentTest.php
@@ -12,7 +12,6 @@ use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
 use Drupal\entity_test\Entity\EntityTest;
-use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
 use Drupal\user\Entity\User;
 use GuzzleHttp\RequestOptions;
 
@@ -23,7 +22,6 @@ use GuzzleHttp\RequestOptions;
  */
 class CommentTest extends ResourceTestBase {
 
-  use BcTimestampNormalizerUnixTestTrait;
   use CommentTestTrait;
 
   /**
@@ -153,12 +151,8 @@ class CommentTest extends ResourceTestBase {
         ],
         'attributes' => [
           'cid' => 1,
-          'created' => 123456789,
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'created' => $this->formatExpectedTimestampItemValues(123456789), */
-          'changed' => $this->entity->getChangedTime(),
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'changed' => $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()), */
+          'created' => '1973-11-29T21:33:09+00:00',
+          'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
           'comment_body' => [
             'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
             'format' => 'plain_text',
diff --git a/tests/src/Functional/EntityTestTest.php b/tests/src/Functional/EntityTestTest.php
index 8398821..14c7d9c 100644
--- a/tests/src/Functional/EntityTestTest.php
+++ b/tests/src/Functional/EntityTestTest.php
@@ -114,9 +114,7 @@ class EntityTestTest extends ResourceTestBase {
         ],
         'attributes' => [
           'id' => 1,
-          'created' => (int) $this->entity->get('created')->value,
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'created' => $this->formatExpectedTimestampItemValues((int) $this->entity->get('created')->value), */
+          'created' => (new \DateTime())->setTimestamp($this->entity->get('created')->value)->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
           'field_test_text' => NULL,
           'langcode' => 'en',
           'name' => 'Llama',
diff --git a/tests/src/Functional/ExternalNormalizersTest.php b/tests/src/Functional/ExternalNormalizersTest.php
index 8f4f21a..dfbd58a 100644
--- a/tests/src/Functional/ExternalNormalizersTest.php
+++ b/tests/src/Functional/ExternalNormalizersTest.php
@@ -10,6 +10,8 @@ use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\user\Entity\Role;
 use Drupal\user\RoleInterface;
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\RequestOptions;
 
 /**
  * Asserts external normalizers are handled as expected by the JSON API module.
@@ -57,9 +59,10 @@ class ExternalNormalizersTest extends BrowserTestBase {
     parent::setUp();
 
     // This test is not about access control at all, so allow anonymous users to
-    // view the test entities.
+    // view and create the test entities.
     Role::load(RoleInterface::ANONYMOUS_ID)
       ->grantPermission('view test entity')
+      ->grantPermission('create entity_test entity_test_with_bundle entities')
       ->save();
 
     FieldStorageConfig::create([
@@ -92,11 +95,16 @@ class ExternalNormalizersTest extends BrowserTestBase {
    *   The expected JSON API normalization of the tested field. Must be either
    *   - static::VALUE_ORIGINAL (normalizer IS NOT expected to override)
    *   - static::VALUE_OVERRIDDEN (normalizer IS expected to override)
+   * @param string $expected_value_jsonapi_denormalization
+   *   The expected JSON API denormalization of the tested field. Must be either
+   *   - static::VALUE_OVERRIDDEN (denormalizer IS NOT expected to override)
+   *   - static::VALUE_ORIGINAL (denormalizer IS expected to override)
    *
    * @dataProvider providerTestFormatAgnosticNormalizers
    */
-  public function testFormatAgnosticNormalizers($test_module, $expected_value_jsonapi_normalization) {
+  public function testFormatAgnosticNormalizers($test_module, $expected_value_jsonapi_normalization, $expected_value_jsonapi_denormalization) {
     assert(in_array($expected_value_jsonapi_normalization, [static::VALUE_ORIGINAL, static::VALUE_OVERRIDDEN], TRUE));
+    assert(in_array($expected_value_jsonapi_denormalization, [static::VALUE_ORIGINAL, static::VALUE_OVERRIDDEN], TRUE));
 
     // Asserts the entity contains the value we set.
     $this->assertSame(static::VALUE_ORIGINAL, $this->entity->field_test->value);
@@ -106,6 +114,13 @@ class ExternalNormalizersTest extends BrowserTestBase {
     $core_normalization = $this->container->get('serializer')->normalize($this->entity);
     $this->assertSame(static::VALUE_ORIGINAL, $core_normalization['field_test'][0]['value']);
 
+    // Asserts denormalizing the entity using core's 'serializer' service DOES
+    // yield the value we set.
+    $core_normalization['field_test'][0]['value'] = static::VALUE_OVERRIDDEN;
+    $denormalized_entity = $this->container->get('serializer')->denormalize($core_normalization, EntityTest::class, 'json', []);
+    $this->assertInstanceOf(EntityTest::class, $denormalized_entity);
+    $this->assertSame(static::VALUE_OVERRIDDEN, $denormalized_entity->field_test->value);
+
     // Install test module that contains a high-priority alternative normalizer.
     $this->container->get('module_installer')->install([$test_module]);
     $this->rebuildContainer();
@@ -115,14 +130,42 @@ class ExternalNormalizersTest extends BrowserTestBase {
     $core_normalization = $this->container->get('serializer')->normalize($this->entity);
     $this->assertSame(static::VALUE_OVERRIDDEN, $core_normalization['field_test'][0]['value']);
 
-    // Asserts that this does NOT affect the JSON API normalization.
+    // Asserts denormalizing the entity using core's 'serializer' service DOES
+    // NOT ANYMORE yield the value we set.
+    $core_normalization = $this->container->get('serializer')->normalize($this->entity);
+    $core_normalization['field_test'][0]['value'] = static::VALUE_OVERRIDDEN;
+    $denormalized_entity = $this->container->get('serializer')->denormalize($core_normalization, EntityTest::class, 'json', []);
+    $this->assertInstanceOf(EntityTest::class, $denormalized_entity);
+    // @todo Make this unconditional once https://www.drupal.org/project/drupal/issues/2957385 lands — JSON API fixed denormalization of properties in https://www.drupal.org/project/jsonapi/issues/2955615, core's Serialization module still has to follow
+    if ($test_module === 'jsonapi_test_field_type') {
+      $this->assertSame(static::VALUE_ORIGINAL, $denormalized_entity->field_test->value);
+    }
+
+    // Asserts the expected JSON API normalization.
     // @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2878463.
     $url = Url::fromRoute('jsonapi.entity_test--entity_test.individual', ['entity_test' => $this->entity->uuid()]);
     /* $url = $this->entity->toUrl('jsonapi'); */
     $client = $this->getSession()->getDriver()->getClient()->getClient();
+    assert($client instanceof ClientInterface);
     $response = $client->request('GET', $url->setAbsolute(TRUE)->toString());
     $document = Json::decode((string) $response->getBody());
     $this->assertSame($expected_value_jsonapi_normalization, $document['data']['attributes']['field_test']);
+
+    // Asserts the expected JSON API denormalization.
+    $request_options = [];
+    $request_options[RequestOptions::BODY] = Json::encode([
+      'data' => [
+        'type' => 'entity_test--entity_test',
+        'attributes' => [
+          'field_test' => static::VALUE_OVERRIDDEN,
+        ],
+      ],
+    ]);
+    $response = $client->request('POST', Url::fromRoute('jsonapi.entity_test--entity_test.collection')->setAbsolute(TRUE)->toString(), $request_options);
+    $document = Json::decode((string) $response->getBody());
+    $this->assertSame(static::VALUE_OVERRIDDEN, $document['data']['attributes']['field_test']);
+    $created_entity = EntityTest::load($document['data']['attributes']['id']);
+    $this->assertSame($expected_value_jsonapi_denormalization, $created_entity->field_test->value);
   }
 
   /**
@@ -134,14 +177,18 @@ class ExternalNormalizersTest extends BrowserTestBase {
   public function providerTestFormatAgnosticNormalizers() {
     return [
       'Format-agnostic @FieldType-level normalizers SHOULD NOT be able to affect the JSON API normalization' => [
-        // \Drupal\jsonapi_test_field_type\Normalizer\StringNormalizer::normalize()
         'jsonapi_test_field_type',
+        // \Drupal\jsonapi_test_field_type\Normalizer\StringNormalizer::normalize()
         static::VALUE_ORIGINAL,
+        // \Drupal\jsonapi_test_field_type\Normalizer\StringNormalizer::denormalize()
+        static::VALUE_OVERRIDDEN,
       ],
       'Format-agnostic @DataType-level normalizers SHOULD be able to affect the JSON API normalization' => [
-        // \Drupal\jsonapi_test_data_type\Normalizer\StringNormalizer::normalize()
         'jsonapi_test_data_type',
+        // \Drupal\jsonapi_test_data_type\Normalizer\StringNormalizer::normalize()
         static::VALUE_OVERRIDDEN,
+        // \Drupal\jsonapi_test_data_type\Normalizer\StringNormalizer::denormalize()
+        static::VALUE_ORIGINAL,
       ],
     ];
   }
diff --git a/tests/src/Functional/FeedTest.php b/tests/src/Functional/FeedTest.php
index 9df2a72..11d99a4 100644
--- a/tests/src/Functional/FeedTest.php
+++ b/tests/src/Functional/FeedTest.php
@@ -4,7 +4,6 @@ namespace Drupal\Tests\jsonapi\Functional;
 
 use Drupal\aggregator\Entity\Feed;
 use Drupal\Core\Url;
-use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
 
 /**
  * JSON API integration test for the "Feed" content entity type.
@@ -13,8 +12,6 @@ use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
  */
 class FeedTest extends ResourceTestBase {
 
-  use BcTimestampNormalizerUnixTestTrait;
-
   /**
    * {@inheritdoc}
    */
@@ -115,18 +112,14 @@ class FeedTest extends ResourceTestBase {
           'url' => 'http://example.com/rss.xml',
           'title' => 'Feed',
           'refresh' => 900,
-          'checked' => 123456789,
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'checked' => $this->formatExpectedTimestampItemValues(123456789), */
-          'queued' => 123456789,
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'queued' => $this->formatExpectedTimestampItemValues(123456789), */
+          'checked' => '1973-11-29T21:33:09+00:00',
+          'queued' => '1973-11-29T21:33:09+00:00',
           'link' => 'http://example.com',
           'description' => 'Feed Resource Test 1',
           'image' => 'http://example.com/feed_logo',
           'hash' => 'abcdefg',
           'etag' => 'hijklmn',
-          'modified' => 123456789,
+          'modified' => '1973-11-29T21:33:09+00:00',
           // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
           /* 'modified' => $this->formatExpectedTimestampItemValues(123456789), */
           'langcode' => 'en',
diff --git a/tests/src/Functional/FileTest.php b/tests/src/Functional/FileTest.php
index 930ad41..7014d4d 100644
--- a/tests/src/Functional/FileTest.php
+++ b/tests/src/Functional/FileTest.php
@@ -129,12 +129,8 @@ class FileTest extends ResourceTestBase {
           'self' => $self_url,
         ],
         'attributes' => [
-          'changed' => $this->entity->getChangedTime(),
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'changed' => $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()), */
-          'created' => (int) $this->entity->getCreatedTime(),
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'created' => $this->formatExpectedTimestampItemValues((int) $this->entity->getCreatedTime()), */
+          'created' => (new \DateTime())->setTimestamp($this->entity->getCreatedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
+          'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
           'fid' => 1,
           'filemime' => 'text/plain',
           'filename' => 'drupal.txt',
diff --git a/tests/src/Functional/MediaTest.php b/tests/src/Functional/MediaTest.php
index 68142c9..0d676af 100644
--- a/tests/src/Functional/MediaTest.php
+++ b/tests/src/Functional/MediaTest.php
@@ -7,7 +7,6 @@ use Drupal\Core\Url;
 use Drupal\file\Entity\File;
 use Drupal\media\Entity\Media;
 use Drupal\media\Entity\MediaType;
-use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
 use Drupal\user\Entity\User;
 
 /**
@@ -17,8 +16,6 @@ use Drupal\user\Entity\User;
  */
 class MediaTest extends ResourceTestBase {
 
-  use BcTimestampNormalizerUnixTestTrait;
-
   /**
    * {@inheritdoc}
    */
@@ -168,15 +165,9 @@ class MediaTest extends ResourceTestBase {
           'langcode' => 'en',
           'name' => 'Llama',
           'status' => TRUE,
-          'created' => 123456789,
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'created' => $this->formatExpectedTimestampItemValues(123456789), */
-          'changed' => $this->entity->getChangedTime(),
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'changed' => $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()), */
-          'revision_created' => (int) $this->entity->getRevisionCreationTime(),
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'revision_created' => $this->formatExpectedTimestampItemValues((int) $this->entity->getRevisionCreationTime()), */
+          'created' => '1973-11-29T21:33:09+00:00',
+          'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
+          'revision_created' => (new \DateTime())->setTimestamp($this->entity->getRevisionCreationTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
           'default_langcode' => TRUE,
           'revision_log_message' => NULL,
           // @todo Attempt to remove this in https://www.drupal.org/project/drupal/issues/2933518.
diff --git a/tests/src/Functional/MenuLinkContentTest.php b/tests/src/Functional/MenuLinkContentTest.php
index cb258fc..2bd350e 100644
--- a/tests/src/Functional/MenuLinkContentTest.php
+++ b/tests/src/Functional/MenuLinkContentTest.php
@@ -4,7 +4,6 @@ namespace Drupal\Tests\jsonapi\Functional;
 
 use Drupal\Core\Url;
 use Drupal\menu_link_content\Entity\MenuLinkContent;
-use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
 
 /**
  * JSON API integration test for the "MenuLinkContent" content entity type.
@@ -13,8 +12,6 @@ use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
  */
 class MenuLinkContentTest extends ResourceTestBase {
 
-  use BcTimestampNormalizerUnixTestTrait;
-
   /**
    * {@inheritdoc}
    */
@@ -99,9 +96,7 @@ class MenuLinkContentTest extends ResourceTestBase {
             'title' => NULL,
             'options' => [],
           ],
-          'changed' => $this->entity->getChangedTime(),
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'changed' => $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()), */
+          'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
           'default_langcode' => TRUE,
           'description' => 'Llama Gabilondo',
           'enabled' => TRUE,
diff --git a/tests/src/Functional/NodeTest.php b/tests/src/Functional/NodeTest.php
index fb611c5..1e7331c 100644
--- a/tests/src/Functional/NodeTest.php
+++ b/tests/src/Functional/NodeTest.php
@@ -8,7 +8,6 @@ use Drupal\Core\Url;
 use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer;
 use Drupal\node\Entity\Node;
 use Drupal\node\Entity\NodeType;
-use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
 use Drupal\user\Entity\User;
 use GuzzleHttp\RequestOptions;
 
@@ -19,8 +18,6 @@ use GuzzleHttp\RequestOptions;
  */
 class NodeTest extends ResourceTestBase {
 
-  use BcTimestampNormalizerUnixTestTrait;
-
   /**
    * {@inheritdoc}
    */
@@ -135,12 +132,8 @@ class NodeTest extends ResourceTestBase {
           'self' => $self_url,
         ],
         'attributes' => [
-          'created' => 123456789,
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'created' => $this->formatExpectedTimestampItemValues(123456789), */
-          'changed' => $this->entity->getChangedTime(),
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'changed' => $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()), */
+          'created' => '1973-11-29T21:33:09+00:00',
+          'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
           'default_langcode' => TRUE,
           'langcode' => 'en',
           'nid' => 1,
@@ -151,9 +144,7 @@ class NodeTest extends ResourceTestBase {
           ],
           'promote' => TRUE,
           'revision_log' => NULL,
-          'revision_timestamp' => 123456789,
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'revision_timestamp' => $this->formatExpectedTimestampItemValues(123456789), */
+          'revision_timestamp' => '1973-11-29T21:33:09+00:00',
           // @todo Attempt to remove this in https://www.drupal.org/project/drupal/issues/2933518.
           'revision_translation_affected' => TRUE,
           'status' => TRUE,
diff --git a/tests/src/Functional/TermTest.php b/tests/src/Functional/TermTest.php
index 2df62f8..156a476 100644
--- a/tests/src/Functional/TermTest.php
+++ b/tests/src/Functional/TermTest.php
@@ -8,7 +8,6 @@ use Drupal\Core\Cache\Cache;
 use Drupal\Core\Url;
 use Drupal\taxonomy\Entity\Term;
 use Drupal\taxonomy\Entity\Vocabulary;
-use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
 use GuzzleHttp\RequestOptions;
 
 /**
@@ -18,8 +17,6 @@ use GuzzleHttp\RequestOptions;
  */
 class TermTest extends ResourceTestBase {
 
-  use BcTimestampNormalizerUnixTestTrait;
-
   /**
    * {@inheritdoc}
    */
@@ -215,9 +212,7 @@ class TermTest extends ResourceTestBase {
           'self' => $self_url,
         ],
         'attributes' => [
-          'changed' => $this->entity->getChangedTime(),
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          /* 'changed' => $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()), */
+          'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
           'default_langcode' => TRUE,
           'description' => [
             'value' => 'It is a little known fact that llamas cannot count higher than seven.',
diff --git a/tests/src/Functional/UserTest.php b/tests/src/Functional/UserTest.php
index acde288..be471e9 100644
--- a/tests/src/Functional/UserTest.php
+++ b/tests/src/Functional/UserTest.php
@@ -6,7 +6,6 @@ use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Url;
 use Drupal\jsonapi\Normalizer\HttpExceptionNormalizer;
-use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
 use Drupal\user\Entity\User;
 use GuzzleHttp\RequestOptions;
 
@@ -17,8 +16,6 @@ use GuzzleHttp\RequestOptions;
  */
 class UserTest extends ResourceTestBase {
 
-  use BcTimestampNormalizerUnixTestTrait;
-
   /**
    * {@inheritdoc}
    */
@@ -132,12 +129,8 @@ class UserTest extends ResourceTestBase {
           'self' => $self_url,
         ],
         'attributes' => [
-          'created' => 123456789,
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          // 'created' => $this->formatExpectedTimestampItemValues(123456789),
-          'changed' => $this->entity->getChangedTime(),
-          // @todo uncomment this in https://www.drupal.org/project/jsonapi/issues/2929932
-          // 'changed' => $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
+          'created' => '1973-11-29T21:33:09+00:00',
+          'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
           'default_langcode' => TRUE,
           'langcode' => 'en',
           'name' => 'Llama',
diff --git a/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php b/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
index 62aae75..9bf4b40 100644
--- a/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
+++ b/tests/src/Kernel/Normalizer/JsonApiDocumentTopLevelNormalizerTest.php
@@ -643,7 +643,6 @@ class JsonApiDocumentTopLevelNormalizerTest extends JsonapiKernelTestBase {
           'type' => 'node--article',
           'attributes' => [
             'title' => 'Testing article',
-            'id' => '33095485-70D2-4E51-A309-535CC5BC0115',
           ],
           'relationships' => [
             'uid' => [
diff --git a/tests/src/Unit/Normalizer/ConfigEntityNormalizerTest.php b/tests/src/Unit/Normalizer/ConfigEntityNormalizerTest.php
index 3d1aab1..406e21a 100644
--- a/tests/src/Unit/Normalizer/ConfigEntityNormalizerTest.php
+++ b/tests/src/Unit/Normalizer/ConfigEntityNormalizerTest.php
@@ -3,7 +3,9 @@
 namespace Drupal\Tests\jsonapi\Unit\Normalizer;
 
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
 use Drupal\jsonapi\Normalizer\ConfigEntityNormalizer;
@@ -39,7 +41,9 @@ class ConfigEntityNormalizerTest extends UnitTestCase {
     $this->normalizer = new ConfigEntityNormalizer(
       $link_manager->reveal(),
       $resource_type_repository->reveal(),
-      $this->prophesize(EntityTypeManagerInterface::class)->reveal()
+      $this->prophesize(EntityTypeManagerInterface::class)->reveal(),
+      $this->prophesize(EntityFieldManagerInterface::class)->reveal(),
+      $this->prophesize(FieldTypePluginManagerInterface::class)->reveal()
     );
   }
 
diff --git a/tests/src/Unit/Normalizer/EntityReferenceFieldNormalizerTest.php b/tests/src/Unit/Normalizer/RelationshipNormalizerTest.php
similarity index 95%
rename from tests/src/Unit/Normalizer/EntityReferenceFieldNormalizerTest.php
rename to tests/src/Unit/Normalizer/RelationshipNormalizerTest.php
index 0319cdd..c4228cf 100644
--- a/tests/src/Unit/Normalizer/EntityReferenceFieldNormalizerTest.php
+++ b/tests/src/Unit/Normalizer/RelationshipNormalizerTest.php
@@ -10,21 +10,21 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Field\FieldTypePluginManagerInterface;
 use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
 use Drupal\field\Entity\FieldConfig;
+use Drupal\jsonapi\Normalizer\RelationshipNormalizer;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
-use Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer;
 use Drupal\jsonapi\LinkManager\LinkManager;
 use Drupal\Tests\UnitTestCase;
 use Prophecy\Argument;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
 /**
- * @coversDefaultClass \Drupal\jsonapi\Normalizer\EntityReferenceFieldNormalizer
+ * @coversDefaultClass \Drupal\jsonapi\Normalizer\RelationshipNormalizer
  * @group jsonapi
  *
  * @internal
  */
-class EntityReferenceFieldNormalizerTest extends UnitTestCase {
+class RelationshipNormalizerTest extends UnitTestCase {
 
   /**
    * The normalizer under test.
@@ -81,11 +81,11 @@ class EntityReferenceFieldNormalizerTest extends UnitTestCase {
     $entity_repository->loadEntityByUuid('lorem', '4e6cb61d-4f04-437f-99fe-42c002393658')
       ->willReturn($entity->reveal());
 
-    $this->normalizer = new EntityReferenceFieldNormalizer(
+    $this->normalizer = new RelationshipNormalizer(
+      $resource_type_repository->reveal(),
       $link_manager->reveal(),
       $field_manager->reveal(),
       $plugin_manager->reveal(),
-      $resource_type_repository->reveal(),
       $entity_repository->reveal()
     );
   }
diff --git a/tests/src/Unit/Routing/RoutesTest.php b/tests/src/Unit/Routing/RoutesTest.php
index 2997bfb..55901e9 100644
--- a/tests/src/Unit/Routing/RoutesTest.php
+++ b/tests/src/Unit/Routing/RoutesTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\jsonapi\Unit\Routing;
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\jsonapi\Normalizer\Relationship;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepository;
 use Drupal\jsonapi\Routing\Routes;
@@ -142,7 +143,7 @@ class RoutesTest extends UnitTestCase {
       'entity_type_1' => ['type' => 'entity:entity_type_1'],
       'resource_type' => ['type' => 'jsonapi_resource_type'],
     ], $route->getOption('parameters'));
-    $this->assertSame('Drupal\Core\Field\EntityReferenceFieldItemList', $route->getDefault('serialization_class'));
+    $this->assertSame(Relationship::class, $route->getDefault('serialization_class'));
   }
 
   /**
