 .../hal/src/Normalizer/FieldItemNormalizer.php     | 33 +++++++-
 .../EntityResource/EntityResourceTestBase.php      | 18 ++++-
 .../src/Normalizer/FieldItemNormalizer.php         | 33 +++++++-
 .../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 +
 11 files changed, 270 insertions(+), 3 deletions(-)

diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
index f5b2ec0..3f35c77 100644
--- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
@@ -3,6 +3,7 @@
 namespace Drupal\hal\Normalizer;
 
 use Drupal\Core\Field\FieldItemInterface;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
 use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
 
@@ -71,7 +72,37 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
    *   The value to use in Entity::setValue().
    */
   protected function constructValue($data, $context) {
-    return $data;
+    $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;
+      $property_value_class = $property_definitions[$item_definition->getMainPropertyName()]->getClass();
+      return $this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $context)
+        ? $this->serializer->denormalize($property_value, $property_value_class, NULL, $context)
+        : $property_value;
+    }
+
+    $data_internal = [];
+    if (!empty($property_definitions)) {
+      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, NULL, $context)
+          ? $this->serializer->denormalize($property_value, $property_value_class, NULL, $context)
+          : $property_value;
+      }
+    }
+    else {
+      $data_internal = $data;
+    }
+
+    return $data_internal;
   }
 
   /**
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index d2d1165..dabc878 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -1575,7 +1575,8 @@ 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();
+        $field_definition = $modified_entity->get($field_name)->getFieldDefinition();
+        $field_type = $field_definition->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
@@ -1583,6 +1584,21 @@ protected function assertStoredEntityMatchesSentNormalization(array $sent_normal
         $expected_field_normalization = ($field_type !== 'map')
           ? static::castToString($field_normalization)
           : $field_normalization;
+        // Denormalize every sent field item property to make it possible to
+        // compare against the stored value.
+        $property_definitions = $field_definition->getItemDefinition()->getPropertyDefinitions();
+        $denormalization_context = ['field_definition' => $field_definition];
+        foreach ($expected_field_normalization as $delta => $expected_field_item_normalization) {
+          foreach ($expected_field_item_normalization as $property_name => $property_value) {
+            if (!array_key_exists($property_name, $property_definitions)) {
+              continue;
+            }
+            $property_value_class = $property_definitions[$property_name]->getClass();
+            $expected_field_normalization[$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;
+          }
+        }
         // 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.
diff --git a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
index decca43..40bf4ee 100644
--- a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php
@@ -3,6 +3,7 @@
 namespace Drupal\serialization\Normalizer;
 
 use Drupal\Core\Field\FieldItemInterface;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinitionInterface;
 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
 use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
@@ -51,7 +52,37 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
    *   The value to use in Entity::setValue().
    */
   protected function constructValue($data, $context) {
-    return $data;
+    $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;
+      $property_value_class = $property_definitions[$item_definition->getMainPropertyName()]->getClass();
+      return $this->serializer->supportsDenormalization($property_value, $property_value_class, NULL, $context)
+        ? $this->serializer->denormalize($property_value, $property_value_class, NULL, $context)
+        : $property_value;
+    }
+
+    $data_internal = [];
+    if (!empty($property_definitions)) {
+      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, NULL, $context)
+          ? $this->serializer->denormalize($property_value, $property_value_class, NULL, $context)
+          : $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());
 
   }
 
