diff --git a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
index e1efc207de9ec62aecdc8e5bb35e6776cfb962bc..6a54b3c50205760e73d381dcc29618ad5dbf80cd 100644
--- a/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
+++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\rest\LinkManager\LinkManagerInterface;
+use Drupal\serialization\Normalizer\FieldableEntityNormalizerTrait;
 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 
 /**
@@ -14,6 +15,8 @@
  */
 class ContentEntityNormalizer extends NormalizerBase {
 
+  use FieldableEntityNormalizerTrait;
+
   /**
    * The interface or class that this Normalizer supports.
    *
@@ -28,13 +31,6 @@ class ContentEntityNormalizer extends NormalizerBase {
    */
   protected $linkManager;
 
-  /**
-   * The entity manager.
-   *
-   * @var \Drupal\Core\Entity\EntityManagerInterface
-   */
-  protected $entityManager;
-
   /**
    * The module handler.
    *
@@ -42,7 +38,6 @@ class ContentEntityNormalizer extends NormalizerBase {
    */
   protected $moduleHandler;
 
-
   /**
    * Constructs an ContentEntityNormalizer object.
    *
@@ -128,7 +123,7 @@ public function denormalize($data, $class, $format = NULL, array $context = arra
 
     // Create the entity.
     $typed_data_ids = $this->getTypedDataIds($data['_links']['type'], $context);
-    $entity_type = $this->entityManager->getDefinition($typed_data_ids['entity_type']);
+    $entity_type = $this->getEntityTypeDefinition($typed_data_ids['entity_type']);
     $default_langcode_key = $entity_type->getKey('default_langcode');
     $langcode_key = $entity_type->getKey('langcode');
     $values = array();
@@ -174,24 +169,12 @@ public function denormalize($data, $class, $format = NULL, array $context = arra
       }
     }
 
+    $this->denormalizeFieldData($data, $entity, $format, $context);
+
     // Pass the names of the fields whose values can be merged.
+    // @todo https://www.drupal.org/node/2456257 remove this.
     $entity->_restSubmittedFields = array_keys($data);
 
-    // Iterate through remaining items in data array. These should all
-    // correspond to fields.
-    foreach ($data as $field_name => $field_data) {
-      $items = $entity->get($field_name);
-      // Remove any values that were set as a part of entity creation (e.g
-      // uuid). If the incoming field data is set to an empty array, this will
-      // also have the effect of emptying the field in REST module.
-      $items->setValue(array());
-      if ($field_data) {
-        // Denormalize the field data into the FieldItemList object.
-        $context['target_instance'] = $items;
-        $this->serializer->denormalize($field_data, get_class($items), $format, $context);
-      }
-    }
-
     return $entity;
   }
 
diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml
index 8b570c076308abff5f3948b3468102b5cb235db5..cfb71adcf99e792b213c55383d25093aaa0f91c5 100644
--- a/core/modules/serialization/serialization.services.yml
+++ b/core/modules/serialization/serialization.services.yml
@@ -25,13 +25,28 @@ services:
     class: Drupal\serialization\Normalizer\EntityReferenceFieldItemNormalizer
     tags:
       # Set the priority lower than the hal entity reference field item
-      # normalizer, so that we do not replace that for hal_json.
+      # normalizer, so that we do not replace that for hal_json but higher than
+      # this modules generic field item normalizer.
       # @todo Find a better way for this in https://www.drupal.org/node/2575761.
-      - { name: normalizer, priority: 5 }
+      - { name: normalizer, priority: 8 }
+  serialization.normalizer.field_item:
+    class: Drupal\serialization\Normalizer\FieldItemNormalizer
+    tags:
+      # Priority must be lower than serializer.normalizer.field_item.hal and any
+      # field type specific normalizer such as
+      # serializer.normalizer.entity_reference_field_item.
+      - { name: normalizer, priority: 6 }
+  serialization.normalizer.field:
+    class: Drupal\serialization\Normalizer\FieldNormalizer
+    tags:
+      # Priority must be lower than serializer.normalizer.field.hal.
+      - { name: normalizer, priority: 6 }
   serializer.normalizer.list:
     class: Drupal\serialization\Normalizer\ListNormalizer
     tags:
-      - { name: normalizer }
+      # Priority must be higher than serialization.normalizer.field but less
+      # than hal field normalizer.
+      - { name: normalizer, priority: 9 }
   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/ContentEntityNormalizer.php b/core/modules/serialization/src/Normalizer/ContentEntityNormalizer.php
index 0fb5300ad890a62c1ad411f85074fdb7992a7eec..b47eeef6cc93be0610d1371f5d4c023b09ce608d 100644
--- a/core/modules/serialization/src/Normalizer/ContentEntityNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/ContentEntityNormalizer.php
@@ -8,9 +8,7 @@
 class ContentEntityNormalizer extends EntityNormalizer {
 
   /**
-   * The interface or class that this Normalizer supports.
-   *
-   * @var array
+   * {@inheritdoc}
    */
   protected $supportedInterfaceOrClass = ['Drupal\Core\Entity\ContentEntityInterface'];
 
diff --git a/core/modules/serialization/src/Normalizer/EntityNormalizer.php b/core/modules/serialization/src/Normalizer/EntityNormalizer.php
index b800e812af35fb67815f2ea68f96644d6c5013eb..84baec94be87fbfdb16780ee33f34244ba0e9613 100644
--- a/core/modules/serialization/src/Normalizer/EntityNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/EntityNormalizer.php
@@ -2,8 +2,9 @@
 
 namespace Drupal\serialization\Normalizer;
 
+use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
-use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+use Drupal\Core\Entity\FieldableEntityInterface;
 use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
@@ -11,19 +12,14 @@
  */
 class EntityNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
 
+  use FieldableEntityNormalizerTrait;
+
   /**
    * The interface or class that this Normalizer supports.
    *
    * @var array
    */
-  protected $supportedInterfaceOrClass = array('Drupal\Core\Entity\EntityInterface');
-
-  /**
-   * The entity manager.
-   *
-   * @var \Drupal\Core\Entity\EntityManagerInterface
-   */
-  protected $entityManager;
+  protected $supportedInterfaceOrClass = [EntityInterface::class];
 
   /**
    * Constructs an EntityNormalizer object.
@@ -39,48 +35,25 @@ public function __construct(EntityManagerInterface $entity_manager) {
    * {@inheritdoc}
    */
   public function denormalize($data, $class, $format = NULL, array $context = []) {
-    // Get the entity type ID while letting context override the $class param.
-    $entity_type_id = !empty($context['entity_type']) ? $context['entity_type'] : $this->entityManager->getEntityTypeFromClass($class);
-
-    /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type_definition */
-    // Get the entity type definition.
-    $entity_type_definition = $this->entityManager->getDefinition($entity_type_id, FALSE);
+    $entity_type_id = $this->determineEntityTypeId($class, $context);
+    $entity_type_definition = $this->getEntityTypeDefinition($entity_type_id);
 
-    // Don't try to create an entity without an entity type id.
-    if (!$entity_type_definition) {
-      throw new UnexpectedValueException(sprintf('The specified entity type "%s" does not exist. A valid etnity type is required for denormalization', $entity_type_id));
-    }
+    // The bundle property will be required to denormalize a bundleable
+    // fieldable entity.
+    if ($entity_type_definition->hasKey('bundle') && $entity_type_definition->isSubclassOf(FieldableEntityInterface::class)) {
+      // Get an array containing the bundle only. This also remove the bundle
+      // key from the $data array.
+      $bundle_data = $this->extractBundleData($data, $entity_type_definition);
 
-    // The bundle property will be required to denormalize a bundleable entity.
-    if ($entity_type_definition->hasKey('bundle')) {
-      $bundle_key = $entity_type_definition->getKey('bundle');
-      // Get the base field definitions for this entity type.
-      $base_field_definitions = $this->entityManager->getBaseFieldDefinitions($entity_type_id);
+      // Create the entity from bundle data only, then apply field values after.
+      $entity = $this->entityManager->getStorage($entity_type_id)->create($bundle_data);
 
-      // Get the ID key from the base field definition for the bundle key or
-      // default to 'value'.
-      $key_id = isset($base_field_definitions[$bundle_key]) ? $base_field_definitions[$bundle_key]->getFieldStorageDefinition()->getMainPropertyName() : 'value';
-
-      // Normalize the bundle if it is not explicitly set.
-      $data[$bundle_key] = isset($data[$bundle_key][0][$key_id]) ? $data[$bundle_key][0][$key_id] : (isset($data[$bundle_key]) ? $data[$bundle_key] : NULL);
-
-      // Get the bundle entity type from the entity type definition.
-      $bundle_type_id = $entity_type_definition->getBundleEntityType();
-      $bundle_types = $bundle_type_id ? $this->entityManager->getStorage($bundle_type_id)->getQuery()->execute() : [];
-
-      // Make sure a bundle has been provided.
-      if (!is_string($data[$bundle_key])) {
-        throw new UnexpectedValueException('A string must be provided as a bundle value.');
-      }
-
-      // Make sure the submitted bundle is a valid bundle for the entity type.
-      if ($bundle_types && !in_array($data[$bundle_key], $bundle_types)) {
-        throw new UnexpectedValueException(sprintf('"%s" is not a valid bundle type for denormalization.', $data[$bundle_key]));
-      }
+      $this->denormalizeFieldData($data, $entity, $format, $context);
+    }
+    else {
+      // Create the entity from all data.
+      $entity = $this->entityManager->getStorage($entity_type_id)->create($data);
     }
-
-    // Create the entity from data.
-    $entity = $this->entityManager->getStorage($entity_type_id)->create($data);
 
     // Pass the names of the fields whose values can be merged.
     // @todo https://www.drupal.org/node/2456257 remove this.
diff --git a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..a3cfcf206f5e64ca8cf87c7a452fe187dbb7e1c3
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\Field\FieldItemInterface;
+use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+/**
+ * Denormalizes field item object structure by updating the entity field values.
+ */
+class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = FieldItemInterface::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = array()) {
+    if (!isset($context['target_instance'])) {
+      throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldItemNormalizer');
+    }
+
+    if ($context['target_instance']->getParent() == NULL) {
+      throw new InvalidArgumentException('The field item passed in via $context[\'target_instance\'] must have a parent set.');
+    }
+
+    /** @var \Drupal\Core\Field\FieldItemInterface $field_item */
+    $field_item = $context['target_instance'];
+
+    $field_item->setValue($this->constructValue($data, $context));
+    return $field_item;
+  }
+
+  /**
+   * Build the field item value using the incoming data.
+   *
+   * Most normalizers that extend this class can simply use this method to
+   * construct the denormalized value without having to override denormalize()
+   * and reimplementing its validation logic or its call to set the field value.
+   *
+   * @param mixed $data
+   *   The incoming data for this field item.
+   * @param array $context
+   *   The context passed into the Normalizer.
+   *
+   * @return mixed
+   *   The value to use in Entity::setValue().
+   */
+  protected function constructValue($data, $context) {
+    return $data;
+  }
+
+}
diff --git a/core/modules/serialization/src/Normalizer/FieldNormalizer.php b/core/modules/serialization/src/Normalizer/FieldNormalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..1847379b23af24402e1bf507f29478f370deadd6
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/FieldNormalizer.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+/**
+ * Denormalizes data to Drupal field values.
+ *
+ * This class simply calls denormalize() on the individual FieldItems. The
+ * FieldItem normalizers are responsible for setting the field values for each
+ * item.
+ *
+ * @see \Drupal\serialization\Normalizer\FieldItemNormalizer.
+ */
+class FieldNormalizer extends ListNormalizer implements DenormalizerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = FieldItemListInterface::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = array()) {
+    if (!isset($context['target_instance'])) {
+      throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldNormalizer');
+    }
+
+    /** @var FieldItemListInterface $items */
+    $items = $context['target_instance'];
+    $item_class = $items->getItemDefinition()->getClass();
+    foreach ($data as $item_data) {
+      // Create a new item and pass it as the target for the unserialization of
+      // $item_data. All items in field should have removed before this method
+      // was called.
+      // @see \Drupal\serialization\Normalizer\ContentEntityNormalizer::denormalize().
+      $context['target_instance'] = $items->appendItem();
+      $this->serializer->denormalize($item_data, $item_class, $format, $context);
+    }
+    return $items;
+  }
+
+}
diff --git a/core/modules/serialization/src/Normalizer/FieldableEntityNormalizerTrait.php b/core/modules/serialization/src/Normalizer/FieldableEntityNormalizerTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..d97d3a4795550e87bb442a20c406c2d2e74a5fc7
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/FieldableEntityNormalizerTrait.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
+
+/**
+ * A trait for providing fieldable entity normalization/denormalization methods.
+ *
+ * @todo Move this into a FieldableEntityNormalizer in Drupal 9. This is a trait
+ *   used in \Drupal\serialization\Normalizer\EntityNormalizer to maintain BC.
+ *   @see https://www.drupal.org/node/2834734
+ */
+trait FieldableEntityNormalizerTrait {
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * Determines the entity type ID to denormalize as.
+   *
+   * @param string $class
+   *   The entity type class to be denormalized to.
+   * @param array $context
+   *   The serialization context data.
+   *
+   * @return string
+   *   The entity type ID.
+   */
+  protected function determineEntityTypeId($class, $context) {
+    // Get the entity type ID while letting context override the $class param.
+    return !empty($context['entity_type']) ? $context['entity_type'] : $this->entityManager->getEntityTypeFromClass($class);
+  }
+
+  /**
+   * Gets the entity type definition.
+   *
+   * @param string $entity_type_id
+   *   The entity type ID to load the definition for.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeInterface
+   *   The loaded entity type definition.
+   *
+   * @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException
+   */
+  protected function getEntityTypeDefinition($entity_type_id) {
+    /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type_definition */
+    // Get the entity type definition.
+    $entity_type_definition = $this->entityManager->getDefinition($entity_type_id, FALSE);
+
+    // Don't try to create an entity without an entity type id.
+    if (!$entity_type_definition) {
+      throw new UnexpectedValueException(sprintf('The specified entity type "%s" does not exist. A valid entity type is required for denormalization', $entity_type_id));
+    }
+
+    return $entity_type_definition;
+  }
+
+  /**
+   * Denormalizes the bundle property so entity creation can use it.
+   *
+   * @param array $data
+   *   The data being denormalized.
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type_definition
+   *   The entity type definition.
+   *
+   * @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException
+   *
+   * @return string
+   *   The valid bundle name.
+   */
+  protected function extractBundleData(array &$data, EntityTypeInterface $entity_type_definition) {
+    $bundle_key = $entity_type_definition->getKey('bundle');
+    // Get the base field definitions for this entity type.
+    $base_field_definitions = $this->entityManager->getBaseFieldDefinitions($entity_type_definition->id());
+
+    // Get the ID key from the base field definition for the bundle key or
+    // default to 'value'.
+    $key_id = isset($base_field_definitions[$bundle_key]) ? $base_field_definitions[$bundle_key]->getFieldStorageDefinition()->getMainPropertyName() : 'value';
+
+    // Normalize the bundle if it is not explicitly set.
+    $bundle_value = isset($data[$bundle_key][0][$key_id]) ? $data[$bundle_key][0][$key_id] : (isset($data[$bundle_key]) ? $data[$bundle_key] : NULL);
+    // Unset the bundle from the data.
+    unset($data[$bundle_key]);
+
+    // Get the bundle entity type from the entity type definition.
+    $bundle_type_id = $entity_type_definition->getBundleEntityType();
+    $bundle_types = $bundle_type_id ? $this->entityManager->getStorage($bundle_type_id)->getQuery()->execute() : [];
+
+    // Make sure a bundle has been provided.
+    if (!is_string($bundle_value)) {
+      throw new UnexpectedValueException('A string must be provided as a bundle value.');
+    }
+
+    // Make sure the submitted bundle is a valid bundle for the entity type.
+    if ($bundle_types && !in_array($bundle_value, $bundle_types)) {
+      throw new UnexpectedValueException(sprintf('"%s" is not a valid bundle type for denormalization.', $bundle_value));
+    }
+
+    return [$bundle_key => $bundle_value];
+  }
+
+  /**
+   * Denormalizes entity data by denormalizing each field individually.
+   *
+   * @param array $data
+   *   The data to denormalize.
+   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
+   *   The fieldable entity to set field values for.
+   * @param string $format
+   *   The serialization format.
+   * @param array $context
+   *   The context data.
+   */
+  protected function denormalizeFieldData(array $data, FieldableEntityInterface $entity, $format, array $context) {
+    foreach ($data as $field_name => $field_data) {
+      $field_item_list = $entity->get($field_name);
+
+      // Remove any values that were set as a part of entity creation (e.g
+      // uuid). If the incoming field data is set to an empty array, this will
+      // also have the effect of emptying the field in REST module.
+      $field_item_list->setValue([]);
+      $field_item_list_class = get_class($field_item_list);
+
+      if ($field_data) {
+        // The field instance must be passed in the context so that the field
+        // denormalizer can update field values for the parent entity.
+        $context['target_instance'] = $field_item_list;
+        $this->serializer->denormalize($field_data, $field_item_list_class, $format, $context);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.info.yml b/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4ba215ecbace96073e3915ac6fb9b3faba515fff
--- /dev/null
+++ b/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.info.yml
@@ -0,0 +1,6 @@
+name: 'FieldItem normalization test support'
+type: module
+description: 'Provides test support for fieldItem normalization test support.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml b/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..36243e795459ea9e204bd7c5adb73aa934360fc0
--- /dev/null
+++ b/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml
@@ -0,0 +1,6 @@
+services:
+  serializer.normalizer.silly_fielditem:
+    class: Drupal\field_normalization_test\Normalization\TextItemSillyNormalizer
+    tags:
+      # The priority must be higher than serialization.normalizer.field_item.
+      - { name: normalizer , priority: 9 }
diff --git a/core/modules/serialization/tests/modules/field_normalization_test/src/Normalization/TextItemSillyNormalizer.php b/core/modules/serialization/tests/modules/field_normalization_test/src/Normalization/TextItemSillyNormalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..7187bdf267366240a8da616ea4a9c3f18f125a2c
--- /dev/null
+++ b/core/modules/serialization/tests/modules/field_normalization_test/src/Normalization/TextItemSillyNormalizer.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\field_normalization_test\Normalization;
+
+use Drupal\serialization\Normalizer\FieldItemNormalizer;
+use Drupal\text\Plugin\Field\FieldType\TextItemBase;
+
+/**
+ * A test TextItem normalizer to test denormalization.
+ */
+class TextItemSillyNormalizer extends FieldItemNormalizer {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = TextItemBase::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = array()) {
+    $data = parent::normalize($object, $format, $context);
+    $data['value'] .= '::silly_suffix';
+    return $data;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function constructValue($data, $context) {
+    $value = parent::constructValue($data, $context);
+    $value['value']  = str_replace('::silly_suffix', '', $value['value']);
+    return $value;
+  }
+
+}
diff --git a/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php b/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..43fc9a90cd3df41be11d0c392927ef543bd33474
--- /dev/null
+++ b/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Drupal\Tests\serialization\Kernel;
+
+use Drupal\entity_test\Entity\EntityTestMulRev;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+
+/**
+ * Test field level normalization process.
+ *
+ * @group serialization
+ */
+class FieldItemSerializationTest extends NormalizerTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = array('serialization', 'system', 'field', 'entity_test', 'text', 'filter', 'user', 'field_normalization_test');
+
+  /**
+   * The class name of the test class.
+   *
+   * @var string
+   */
+  protected $entityClass = 'Drupal\entity_test\Entity\EntityTestMulRev';
+
+  /**
+   * The test values.
+   *
+   * @var array
+   */
+  protected $values;
+
+  /**
+   * The test entity.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityBase
+   */
+  protected $entity;
+
+  /**
+   * The serializer service.
+   *
+   * @var \Symfony\Component\Serializer\Serializer.
+   */
+  protected $serializer;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Auto-create a field for testing default field values.
+    FieldStorageConfig::create(array(
+      'entity_type' => 'entity_test_mulrev',
+      'field_name' => 'field_test_text_default',
+      'type' => 'text',
+      'cardinality' => 1,
+      'translatable' => FALSE,
+    ))->save();
+    FieldConfig::create(array(
+      'entity_type' => 'entity_test_mulrev',
+      'field_name' => 'field_test_text_default',
+      'bundle' => 'entity_test_mulrev',
+      'label' => 'Test text-field with default',
+      'default_value' => [
+        [
+          'value' => 'This is the default',
+          'format' => 'full_html',
+        ],
+      ],
+      'widget' => array(
+        'type' => 'text_textfield',
+        'weight' => 0,
+      ),
+    ))->save();
+
+    // Create a test entity to serialize.
+    $this->values = array(
+      'name' => $this->randomMachineName(),
+      'field_test_text' => array(
+        'value' => $this->randomMachineName(),
+        'format' => 'full_html',
+      ),
+    );
+    $this->entity = EntityTestMulRev::create($this->values);
+    $this->entity->save();
+
+    $this->serializer = $this->container->get('serializer');
+
+    $this->installConfig(array('field'));
+  }
+
+  /**
+   * Tests normalizing and denormalizing an entity with field item normalizer.
+   */
+  public function testFieldNormalizeDenormalize() {
+    $normalized = $this->serializer->normalize($this->entity, 'json');
+
+    $expected_field_value = $this->entity->field_test_text[0]->getValue()['value'] . '::silly_suffix';
+    $this->assertEquals($expected_field_value, $normalized['field_test_text'][0]['value'], 'Text field item normalized');
+    $denormalized = $this->serializer->denormalize($normalized, $this->entityClass, 'json');
+
+    $this->assertEquals($denormalized->field_test_text[0]->getValue(), $this->entity->field_test_text[0]->getValue(), 'Text field item denormalized.');
+    $this->assertEquals($denormalized->field_test_text_default[0]->getValue(), $this->entity->field_test_text_default[0]->getValue(), 'Text field item with default denormalized.');
+
+    // Unset the values for text field that has a default value.
+    unset($normalized['field_test_text_default']);
+    $denormalized_without_all_fields = $this->serializer->denormalize($normalized, $this->entityClass, 'json');
+    // Check that denormalized entity is still the same even if not all fields
+    // are not provided.
+    $this->assertEquals($denormalized_without_all_fields->field_test_text[0]->getValue(), $this->entity->field_test_text[0]->getValue(), 'Text field item denormalized.');
+    // Even though field_test_text_default value was unset before
+    // denormalization it should still have the default values for the field.
+    $this->assertEquals($denormalized_without_all_fields->field_test_text_default[0]->getValue(), $this->entity->field_test_text_default[0]->getValue(), 'Text field item with default denormalized.');
+  }
+
+}
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php
index 702107015501febd584c71bed3a939ea768a59e3..b3b2780fd0118f1c3c985e45258231f5320aaddd 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityNormalizerTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\serialization\Unit\Normalizer;
 
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\serialization\Normalizer\EntityNormalizer;
 use Drupal\Tests\UnitTestCase;
 
@@ -104,6 +106,10 @@ public function testDenormalizeWithValidBundle() {
     ];
 
     $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
+
+    $entity_type->expects($this->once())
+      ->method('id')
+      ->willReturn('test');
     $entity_type->expects($this->once())
       ->method('hasKey')
       ->with('bundle')
@@ -112,6 +118,11 @@ public function testDenormalizeWithValidBundle() {
       ->method('getKey')
       ->with('bundle')
       ->will($this->returnValue('test_type'));
+    $entity_type->expects($this->once())
+      ->method('isSubClassOf')
+      ->with(FieldableEntityInterface::class)
+      ->willReturn(TRUE);
+
     $entity_type->expects($this->once())
       ->method('getBundleEntityType')
       ->will($this->returnValue('test_bundle'));
@@ -154,24 +165,50 @@ public function testDenormalizeWithValidBundle() {
       ->with('test_bundle')
       ->will($this->returnValue($entity_type_storage));
 
-    // The expected test data should have a modified test_type property.
+    $key_1 = $this->getMock(FieldItemListInterface::class);
+    $key_2 = $this->getMock(FieldItemListInterface::class);
+
+    $entity = $this->getMock(FieldableEntityInterface::class);
+    $entity->expects($this->at(0))
+      ->method('get')
+      ->with('key_1')
+      ->willReturn($key_1);
+    $entity->expects($this->at(1))
+      ->method('get')
+      ->with('key_2')
+      ->willReturn($key_2);
+
+    $storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
+    // Create should only be called with the bundle property at first.
     $expected_test_data = array(
-      'key_1' => 'value_1',
-      'key_2' => 'value_2',
       'test_type' => 'test_bundle',
     );
 
-    $storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
     $storage->expects($this->once())
       ->method('create')
       ->with($expected_test_data)
-      ->will($this->returnValue($this->getMock('Drupal\Core\Entity\EntityInterface')));
+      ->will($this->returnValue($entity));
 
     $this->entityManager->expects($this->at(3))
       ->method('getStorage')
       ->with('test')
       ->will($this->returnValue($storage));
 
+    // Setup expectations for the serializer. This will be called for each field
+    // item.
+    $serializer = $this->getMockBuilder('Symfony\Component\Serializer\Serializer')
+      ->disableOriginalConstructor()
+      ->setMethods(array('denormalize'))
+      ->getMock();
+    $serializer->expects($this->at(0))
+      ->method('denormalize')
+      ->with('value_1', get_class($key_1), NULL, ['target_instance' => $key_1, 'entity_type' => 'test']);
+    $serializer->expects($this->at(1))
+      ->method('denormalize')
+      ->with('value_2', get_class($key_2), NULL, ['target_instance' => $key_2, 'entity_type' => 'test']);
+
+    $this->entityNormalizer->setSerializer($serializer);
+
     $this->assertNotNull($this->entityNormalizer->denormalize($test_data, 'Drupal\Core\Entity\ContentEntityBase', NULL, ['entity_type' => 'test']));
   }
 
@@ -192,6 +229,10 @@ public function testDenormalizeWithInvalidBundle() {
     ];
 
     $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
+
+    $entity_type->expects($this->once())
+      ->method('id')
+      ->willReturn('test');
     $entity_type->expects($this->once())
       ->method('hasKey')
       ->with('bundle')
@@ -200,6 +241,11 @@ public function testDenormalizeWithInvalidBundle() {
       ->method('getKey')
       ->with('bundle')
       ->will($this->returnValue('test_type'));
+    $entity_type->expects($this->once())
+      ->method('isSubClassOf')
+      ->with(FieldableEntityInterface::class)
+      ->willReturn(TRUE);
+
     $entity_type->expects($this->once())
       ->method('getBundleEntityType')
       ->will($this->returnValue('test_bundle'));
@@ -242,8 +288,7 @@ public function testDenormalizeWithInvalidBundle() {
       ->with('test_bundle')
       ->will($this->returnValue($entity_type_storage));
 
-
-    $this->entityNormalizer->denormalize($test_data, 'Drupal\Core\Entity\ContentEntityBase', NULL, ['entity_type' => 'test']);
+    $this->assertNotNull($this->entityNormalizer->denormalize($test_data, 'Drupal\Core\Entity\ContentEntityBase', NULL, ['entity_type' => 'test']));
   }
 
   /**