 .../hal/src/Normalizer/FieldItemNormalizer.php     | 21 ++----
 .../EntityResource/EntityResourceTestBase.php      | 43 ++++++++---
 .../src/Normalizer/FieldItemNormalizer.php         | 23 +-----
 .../Normalizer/FieldableEntityNormalizerTrait.php  | 69 +++++++++++++++++
 .../src/Normalizer/BooleanNormalizer.php           | 36 +++++++++
 ...test_datatype_boolean_emoji_normalizer.info.yml |  6 ++
 ..._datatype_boolean_emoji_normalizer.services.yml |  6 ++
 .../src/Normalizer/BooleanItemNormalizer.php       | 39 ++++++++++
 ...est_fieldtype_boolean_emoji_normalizer.info.yml |  6 ++
 ...fieldtype_boolean_emoji_normalizer.services.yml |  6 ++
 .../src/Kernel/FieldItemSerializationTest.php      | 87 ++++++++++++++++++++++
 .../EntityReferenceFieldItemNormalizerTest.php     |  3 +
 12 files changed, 302 insertions(+), 43 deletions(-)

diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
index f5b2ec0..047a09f 100644
--- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
@@ -3,7 +3,9 @@
 namespace Drupal\hal\Normalizer;
 
 use Drupal\Core\Field\FieldItemInterface;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
 use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
+use Drupal\serialization\Normalizer\FieldableEntityNormalizerTrait;
 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
 
 /**
@@ -11,6 +13,10 @@
  */
 class FieldItemNormalizer extends NormalizerBase {
 
+  use FieldableEntityNormalizerTrait {
+    constructValue as protected;
+  }
+
   /**
    * The interface or class that this Normalizer supports.
    *
@@ -60,21 +66,6 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
   }
 
   /**
-   * Build the field item value using the incoming data.
-   *
-   * @param $data
-   *   The incoming data for this field item.
-   * @param $context
-   *   The context passed into the Normalizer.
-   *
-   * @return mixed
-   *   The value to use in Entity::setValue().
-   */
-  protected function constructValue($data, $context) {
-    return $data;
-  }
-
-  /**
    * Normalizes field values for an item.
    *
    * @param \Drupal\Core\Field\FieldItemInterface $field_item
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index d2d1165..993957e 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -1575,18 +1575,43 @@ protected function assertStoredEntityMatchesSentNormalization(array $sent_normal
       // Some top-level keys in the normalization may not be fields on the
       // entity (for example '_links' and '_embedded' in the HAL normalization).
       if ($modified_entity->hasField($field_name)) {
-        $field_type = $modified_entity->get($field_name)->getFieldDefinition()->getType();
-        // Fields are stored in the database, when read they are represented
-        // as strings in PHP memory. The exception: field types that are
-        // stored in a serialized way. Hence we need to cast most expected
-        // field normalizations to strings.
-        $expected_field_normalization = ($field_type !== 'map')
-          ? static::castToString($field_normalization)
-          : $field_normalization;
+        $field_definition = $modified_entity->get($field_name)->getFieldDefinition();
+        $property_definitions = $field_definition->getItemDefinition()->getPropertyDefinitions();
+        $expected_stored_data = [];
+        // Some fields don't have any property definitions, so there's nothing
+        // to denormalize.
+        if (empty($property_definitions)) {
+          $expected_stored_data = $field_normalization;
+        }
+        else {
+          // Denormalize every sent field item property to make it possible to
+          // compare against the stored value.
+          $denormalization_context = ['field_definition' => $field_definition];
+          foreach ($field_normalization as $delta => $expected_field_item_normalization) {
+            foreach ($property_definitions as $property_name => $property_definition) {
+              // Not every property is required to be sent.
+              if (!array_key_exists($property_name, $field_normalization[$delta])) {
+                continue;
+              }
+              // Computed properties are not stored.
+              if ($property_definition->isComputed()) {
+                continue;
+              }
+              $property_value = $field_normalization[$delta][$property_name];
+              $property_value_class = $property_definitions[$property_name]->getClass();
+              $expected_stored_data[$delta][$property_name] = $this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $denormalization_context)
+                ? $this->serializer->denormalize($property_value, $property_value_class, NULL, $denormalization_context)
+                : $property_value;
+            }
+          }
+          // Fields are stored in the database, when read they are represented
+          // as strings in PHP memory.
+          $expected_stored_data = static::castToString($expected_stored_data);
+        }
         // Subset, not same, because we can e.g. send just the target_id for the
         // bundle in a PATCH or POST request; the response will include more
         // properties.
-        $this->assertArraySubset($expected_field_normalization, $modified_entity->get($field_name)->getValue(), TRUE);
+        $this->assertArraySubset($expected_stored_data, $modified_entity->get($field_name)->getValue(), TRUE);
       }
     }
   }
diff --git a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
index decca43..014cd30 100644
--- a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
@@ -11,6 +11,10 @@
  */
 class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
 
+  use FieldableEntityNormalizerTrait {
+    constructValue as protected;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -35,23 +39,4 @@ public function denormalize($data, $class, $format = NULL, array $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/FieldableEntityNormalizerTrait.php b/core/modules/serialization/src/Normalizer/FieldableEntityNormalizerTrait.php
index c8211a8..b84d1c1 100644
--- a/core/modules/serialization/src/Normalizer/FieldableEntityNormalizerTrait.php
+++ b/core/modules/serialization/src/Normalizer/FieldableEntityNormalizerTrait.php
@@ -4,6 +4,8 @@
 
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\FieldItemInterface;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 
 /**
@@ -137,4 +139,71 @@ protected function denormalizeFieldData(array $data, FieldableEntityInterface $e
     }
   }
 
+  /**
+   * 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.
+   *
+   * It's recommended to not override this and instead provide a (de)normalizer
+   * at the DataType level.
+   *
+   * @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) {
+    $field_item = $context['target_instance'];
+
+    // Get the property definitions.
+    assert($field_item instanceof FieldItemInterface);
+    $field_definition = $field_item->getFieldDefinition();
+    $item_definition = $field_definition->getItemDefinition();
+    assert($item_definition instanceof FieldItemDataDefinitionInterface);
+    $property_definitions = $item_definition->getPropertyDefinitions();
+
+    if (!is_array($data)) {
+      $property_value = $data;
+      $main_property_name = $item_definition->getMainPropertyName();
+      if ($main_property_name === NULL) {
+        return $property_value;
+      }
+      $property_value_class = $property_definitions[$main_property_name]->getClass();
+      if ($this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $context)) {
+        return $this->serializer->denormalize($property_value, $property_value_class, NULL, $context);
+      }
+      else {
+        return $property_value;
+      }
+    }
+
+    $data_internal = [];
+    if (!empty($property_definitions)) {
+      foreach ($property_definitions as $property_name => $property_definition) {
+        // Not every property is required to be sent.
+        if (!array_key_exists($property_name, $data)) {
+          continue;
+        }
+        $property_value = $data[$property_name];
+        $property_value_class = $property_definition->getClass();
+        if ($this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $context)) {
+          $data_internal[$property_name] = $this->serializer->denormalize($property_value, $property_value_class, NULL, $context);
+        }
+        else {
+          $data_internal[$property_name] = $property_value;
+        }
+      }
+    }
+    else {
+      $data_internal = $data;
+    }
+
+    return $data_internal;
+  }
+
 }
diff --git a/core/modules/serialization/tests/modules/test_datatype_boolean_emoji_normalizer/src/Normalizer/BooleanNormalizer.php b/core/modules/serialization/tests/modules/test_datatype_boolean_emoji_normalizer/src/Normalizer/BooleanNormalizer.php
new file mode 100644
index 0000000..b1b1bd8
--- /dev/null
+++ b/core/modules/serialization/tests/modules/test_datatype_boolean_emoji_normalizer/src/Normalizer/BooleanNormalizer.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\test_datatype_boolean_emoji_normalizer\Normalizer;
+
+use Drupal\Core\TypedData\Plugin\DataType\BooleanData;
+use Drupal\serialization\Normalizer\NormalizerBase;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+/**
+ * Normalizes boolean data weirdly: renders them as 👍 (TRUE) or 👎 (FALSE).
+ */
+class BooleanNormalizer extends NormalizerBase implements DenormalizerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = BooleanData::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = []) {
+    return $object->getValue() ? '👍' : '👎';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    if (!in_array($data, ['👍', '👎'], TRUE)) {
+      throw new \UnexpectedValueException('Only 👍 and 👎 are acceptable values.');
+    }
+    return $data === '👍';
+  }
+
+}
diff --git a/core/modules/serialization/tests/modules/test_datatype_boolean_emoji_normalizer/test_datatype_boolean_emoji_normalizer.info.yml b/core/modules/serialization/tests/modules/test_datatype_boolean_emoji_normalizer/test_datatype_boolean_emoji_normalizer.info.yml
new file mode 100644
index 0000000..0dabae3
--- /dev/null
+++ b/core/modules/serialization/tests/modules/test_datatype_boolean_emoji_normalizer/test_datatype_boolean_emoji_normalizer.info.yml
@@ -0,0 +1,6 @@
+name: 'Test @DataType normalizer'
+type: module
+description: 'Provides test support for @DataType-level normalization.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/serialization/tests/modules/test_datatype_boolean_emoji_normalizer/test_datatype_boolean_emoji_normalizer.services.yml b/core/modules/serialization/tests/modules/test_datatype_boolean_emoji_normalizer/test_datatype_boolean_emoji_normalizer.services.yml
new file mode 100644
index 0000000..5cde957
--- /dev/null
+++ b/core/modules/serialization/tests/modules/test_datatype_boolean_emoji_normalizer/test_datatype_boolean_emoji_normalizer.services.yml
@@ -0,0 +1,6 @@
+services:
+  serializer.normalizer.boolean.datatype.emoji:
+    class: Drupal\test_datatype_boolean_emoji_normalizer\Normalizer\BooleanNormalizer
+    tags:
+      # The priority must be higher than serializer.normalizer.primitive_data.
+      - { name: normalizer , priority: 1000 }
diff --git a/core/modules/serialization/tests/modules/test_fieldtype_boolean_emoji_normalizer/src/Normalizer/BooleanItemNormalizer.php b/core/modules/serialization/tests/modules/test_fieldtype_boolean_emoji_normalizer/src/Normalizer/BooleanItemNormalizer.php
new file mode 100644
index 0000000..57139cc
--- /dev/null
+++ b/core/modules/serialization/tests/modules/test_fieldtype_boolean_emoji_normalizer/src/Normalizer/BooleanItemNormalizer.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\test_fieldtype_boolean_emoji_normalizer\Normalizer;
+
+use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem;
+use Drupal\serialization\Normalizer\FieldItemNormalizer;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+
+/**
+ * Normalizes boolean fields weirdly: renders them as 👍 (TRUE) or 👎 (FALSE).
+ */
+class BooleanItemNormalizer extends FieldItemNormalizer implements DenormalizerInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = BooleanItem::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = []) {
+    $data = parent::normalize($object, $format, $context);
+    $data['value'] = $data['value'] ? '👍' : '👎';
+    return $data;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function constructValue($data, $context) {
+    if (!in_array($data['value'], ['👍', '👎'], TRUE)) {
+      throw new \UnexpectedValueException('Only 👍 and 👎 are acceptable values.');
+    }
+    $data['value'] = ($data['value'] === '👍');
+    return $data;
+  }
+
+}
diff --git a/core/modules/serialization/tests/modules/test_fieldtype_boolean_emoji_normalizer/test_fieldtype_boolean_emoji_normalizer.info.yml b/core/modules/serialization/tests/modules/test_fieldtype_boolean_emoji_normalizer/test_fieldtype_boolean_emoji_normalizer.info.yml
new file mode 100644
index 0000000..e6fa999
--- /dev/null
+++ b/core/modules/serialization/tests/modules/test_fieldtype_boolean_emoji_normalizer/test_fieldtype_boolean_emoji_normalizer.info.yml
@@ -0,0 +1,6 @@
+name: 'Test @FieldType normalizer'
+type: module
+description: 'Provides test support for @FieldType-level normalization.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/serialization/tests/modules/test_fieldtype_boolean_emoji_normalizer/test_fieldtype_boolean_emoji_normalizer.services.yml b/core/modules/serialization/tests/modules/test_fieldtype_boolean_emoji_normalizer/test_fieldtype_boolean_emoji_normalizer.services.yml
new file mode 100644
index 0000000..8001a6f
--- /dev/null
+++ b/core/modules/serialization/tests/modules/test_fieldtype_boolean_emoji_normalizer/test_fieldtype_boolean_emoji_normalizer.services.yml
@@ -0,0 +1,6 @@
+services:
+  serializer.normalizer.boolean.fieldtype.emoji:
+    class: Drupal\test_fieldtype_boolean_emoji_normalizer\Normalizer\BooleanItemNormalizer
+    tags:
+      # The priority must be higher than serialization.normalizer.field_item.
+      - { name: normalizer , priority: 1000 }
diff --git a/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php b/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php
index 403f41a..2f5a9fe 100644
--- a/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php
+++ b/core/modules/serialization/tests/src/Kernel/FieldItemSerializationTest.php
@@ -77,6 +77,19 @@ protected function setUp() {
         'weight' => 0,
       ],
     ])->save();
+    FieldStorageConfig::create([
+      'entity_type' => 'entity_test_mulrev',
+      'field_name' => 'field_test_boolean',
+      'type' => 'boolean',
+      'cardinality' => 1,
+      'translatable' => FALSE,
+    ])->save();
+    FieldConfig::create([
+      'entity_type' => 'entity_test_mulrev',
+      'field_name' => 'field_test_boolean',
+      'bundle' => 'entity_test_mulrev',
+      'label' => 'Test boolean',
+    ])->save();
 
     // Create a test entity to serialize.
     $this->values = [
@@ -85,6 +98,9 @@ protected function setUp() {
         'value' => $this->randomMachineName(),
         'format' => 'full_html',
       ],
+      'field_test_boolean' => [
+        'value' => FALSE,
+      ],
     ];
     $this->entity = EntityTestMulRev::create($this->values);
     $this->entity->save();
@@ -132,4 +148,75 @@ public function testFieldDenormalizeWithScalarValue() {
     $this->serializer->denormalize($normalized, $this->entityClass, 'json');
   }
 
+  /**
+   * Tests a format-agnostic normalizer.
+   *
+   * @param string[] $test_modules
+   *   The test modules to install.
+   * @param string $format
+   *   The format to test. (NULL results in the format-agnostic normalization.)
+   *
+   * @dataProvider providerTestCustomBooleanNormalization
+   */
+  public function testCustomBooleanNormalization(array $test_modules, $format) {
+    // Asserts the entity contains the value we set.
+    $this->assertSame(FALSE, $this->entity->field_test_boolean->value);
+
+    // Asserts normalizing the entity using core's 'serializer' service DOES
+    // yield the value we set.
+    $core_normalization = $this->container->get('serializer')->normalize($this->entity, $format);
+    $this->assertSame(FALSE, $core_normalization['field_test_boolean'][0]['value']);
+
+    // Asserts denormalizing the entity DOES yield the value we set.
+    $core_normalization['field_test_boolean'][0]['value'] = TRUE;
+    $denormalized_entity = $this->container->get('serializer')->denormalize($core_normalization, EntityTestMulRev::class, $format, []);
+    $this->assertInstanceOf(EntityTestMulRev::class, $denormalized_entity);
+    $this->assertSame(TRUE, $denormalized_entity->field_test_boolean->value);
+
+    // Install test module that contains a high-priority alternative normalizer.
+    $this->enableModules($test_modules);
+
+    // Asserts normalizing the entity DOES NOT ANYMORE yield the value we set.
+    $core_normalization = $this->container->get('serializer')->normalize($this->entity, $format);
+    $this->assertSame('👎', $core_normalization['field_test_boolean'][0]['value']);
+
+    // Asserts denormalizing the entity DOES NOT ANYMORE yield the value we set.
+    $core_normalization = $this->container->get('serializer')->normalize($this->entity, $format);
+    $core_normalization['field_test_boolean'][0]['value'] = '👍';
+    $denormalized_entity = $this->container->get('serializer')->denormalize($core_normalization, EntityTestMulRev::class, $format, []);
+    $this->assertInstanceOf(EntityTestMulRev::class, $denormalized_entity);
+    $this->assertSame(TRUE, $denormalized_entity->field_test_boolean->value);
+  }
+
+  /**
+   * Data provider.
+   *
+   * @return array
+   *   Test cases.
+   */
+  public function providerTestCustomBooleanNormalization() {
+    return [
+      'Format-agnostic @FieldType-level normalizers SHOULD be able to affect the format-agnostic normalization' => [
+        ['test_fieldtype_boolean_emoji_normalizer'],
+        NULL,
+      ],
+      'Format-agnostic @DataType-level normalizers SHOULD be able to affect the format-agnostic  normalization' => [
+        ['test_datatype_boolean_emoji_normalizer'],
+        NULL,
+      ],
+      'Format-agnostic @FieldType-level normalizers SHOULD be able to affect the JSON normalization' => [
+        ['test_fieldtype_boolean_emoji_normalizer'],
+        'json',
+      ],
+      'Format-agnostic @DataType-level normalizers SHOULD be able to affect the JSON normalization' => [
+        ['test_datatype_boolean_emoji_normalizer'],
+        'json',
+      ],
+      'Format-agnostic @DataType-level normalizers SHOULD be able to affect the HAL+JSON normalization' => [
+        ['test_datatype_boolean_emoji_normalizer', 'hal'],
+        'hal_json',
+      ],
+    ];
+  }
+
 }
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
index a2cf635..e1ff1e8 100644
--- a/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/EntityReferenceFieldItemNormalizerTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
 use Drupal\Core\GeneratedUrl;
 use Drupal\Core\TypedData\Type\IntegerInterface;
 use Drupal\Core\TypedData\TypedDataInterface;
@@ -83,6 +84,8 @@ protected function setUp() {
       ->willReturn(new \ArrayIterator(['target_id' => []]));
 
     $this->fieldDefinition = $this->prophesize(FieldDefinitionInterface::class);
+    $this->fieldDefinition->getItemDefinition()
+      ->willReturn($this->prophesize(FieldItemDataDefinition::class)->reveal());
 
   }
 
