diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
index 376a444b65..238557af2d 100644
--- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
@@ -21,7 +21,14 @@ class FieldItemNormalizer extends NormalizerBase {
    * {@inheritdoc}
    */
   public function normalize($field_item, $format = NULL, array $context = array()) {
-    $values = $field_item->toArray();
+    $values = [];
+    // We normalize each individual property, so each can do their own casting,
+    // if needed.
+    /** @var \Drupal\Core\TypedData\TypedDataInterface $property */
+    foreach ($field_item as $property_name => $property) {
+      $values[$property_name] = $this->serializer->normalize($property, $format, $context);
+    }
+
     if (isset($context['langcode'])) {
       $values['lang'] = $context['langcode'];
     }
diff --git a/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php b/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php
index 42d7a36a6f..d6978e656f 100644
--- a/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php
+++ b/core/modules/hal/tests/src/Kernel/FileNormalizeTest.php
@@ -2,16 +2,7 @@
 
 namespace Drupal\Tests\hal\Kernel;
 
-use Drupal\Core\Cache\MemoryBackend;
 use Drupal\file\Entity\File;
-use Drupal\hal\Encoder\JsonEncoder;
-use Drupal\hal\Normalizer\FieldItemNormalizer;
-use Drupal\hal\Normalizer\FileEntityNormalizer;
-use Drupal\rest\LinkManager\LinkManager;
-use Drupal\rest\LinkManager\RelationLinkManager;
-use Drupal\rest\LinkManager\TypeLinkManager;
-use Symfony\Component\Serializer\Serializer;
-
 
 /**
  * Tests that file entities can be normalized in HAL.
@@ -33,20 +24,6 @@ class FileNormalizeTest extends NormalizerTestBase {
   protected function setUp() {
     parent::setUp();
     $this->installEntitySchema('file');
-
-    $entity_manager = \Drupal::entityManager();
-    $link_manager = new LinkManager(new TypeLinkManager(new MemoryBackend(), \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack'), \Drupal::service('entity_type.bundle.info')), new RelationLinkManager(new MemoryBackend(), $entity_manager, \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack')));
-
-    // Set up the mock serializer.
-    $normalizers = array(
-      new FieldItemNormalizer(),
-      new FileEntityNormalizer($entity_manager, \Drupal::httpClient(), $link_manager, \Drupal::moduleHandler()),
-    );
-
-    $encoders = array(
-      new JsonEncoder(),
-    );
-    $this->serializer = new Serializer($normalizers, $encoders);
   }
 
 
diff --git a/core/modules/hal/tests/src/Kernel/NormalizerTestBase.php b/core/modules/hal/tests/src/Kernel/NormalizerTestBase.php
index 58674763c2..8c35df07f0 100644
--- a/core/modules/hal/tests/src/Kernel/NormalizerTestBase.php
+++ b/core/modules/hal/tests/src/Kernel/NormalizerTestBase.php
@@ -2,22 +2,9 @@
 
 namespace Drupal\Tests\hal\Kernel;
 
-use Drupal\Core\Cache\MemoryBackend;
 use Drupal\field\Entity\FieldConfig;
-use Drupal\hal\Encoder\JsonEncoder;
-use Drupal\hal\Normalizer\ContentEntityNormalizer;
-use Drupal\hal\Normalizer\EntityReferenceItemNormalizer;
-use Drupal\hal\Normalizer\FieldItemNormalizer;
-use Drupal\hal\Normalizer\FieldNormalizer;
 use Drupal\language\Entity\ConfigurableLanguage;
-use Drupal\rest\LinkManager\LinkManager;
-use Drupal\rest\LinkManager\RelationLinkManager;
-use Drupal\rest\LinkManager\TypeLinkManager;
-use Drupal\serialization\EntityResolver\ChainEntityResolver;
-use Drupal\serialization\EntityResolver\TargetIdResolver;
-use Drupal\serialization\EntityResolver\UuidResolver;
 use Drupal\KernelTests\KernelTestBase;
-use Symfony\Component\Serializer\Serializer;
 use Drupal\field\Entity\FieldStorageConfig;
 
 /**
@@ -130,23 +117,7 @@ protected function setUp() {
       'translatable' => TRUE,
     ])->save();
 
-    $entity_manager = \Drupal::entityManager();
-    $link_manager = new LinkManager(new TypeLinkManager(new MemoryBackend(), \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack'), \Drupal::service('entity_type.bundle.info')), new RelationLinkManager(new MemoryBackend(), $entity_manager, \Drupal::moduleHandler(), \Drupal::service('config.factory'), \Drupal::service('request_stack')));
-
-    $chain_resolver = new ChainEntityResolver(array(new UuidResolver($entity_manager), new TargetIdResolver()));
-
-    // Set up the mock serializer.
-    $normalizers = array(
-      new ContentEntityNormalizer($link_manager, $entity_manager, \Drupal::moduleHandler()),
-      new EntityReferenceItemNormalizer($link_manager, $chain_resolver),
-      new FieldItemNormalizer(),
-      new FieldNormalizer(),
-    );
-
-    $encoders = array(
-      new JsonEncoder(),
-    );
-    $this->serializer = new Serializer($normalizers, $encoders);
+    $this->serializer = $this->container->get('serializer');
   }
 
 }
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index a5cb3617ad..2f69de36dd 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -9,6 +9,8 @@
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\TypedData\PrimitiveInterface;
 use Drupal\rest\Plugin\ResourceBase;
 use Drupal\rest\ResourceResponse;
 use Psr\Log\LoggerInterface;
@@ -178,6 +180,39 @@ public function post(EntityInterface $entity = NULL) {
     }
   }
 
+  /**
+   * Returns the casted value of a field item list.
+   *
+   * Currently just primitive data knows how to cast itself to its actual value.
+   * Given that REST retrieves potential actual integers, but the DB stores
+   * strings, see https://www.drupal.org/node/2310089 we need to cast all values
+   * so we can compare properly that something changed.
+   *
+   * @param \Drupal\Core\Field\FieldItemListInterface $field_item_list
+   *   The field item list to retrieve its data from.
+   *
+   * @return mixed[][]
+   *   The casted value from the field item list.
+   */
+  protected function getCastedValueFromFieldItemList(FieldItemListInterface $field_item_list) {
+    $value = $field_item_list->getValue();
+
+    foreach ($value as $delta => $field_item_value) {
+      /** @var \Drupal\Core\Field\FieldItemInterface $field_item */
+      $field_item = $field_item_list->get($delta);
+      $properties = $field_item->getProperties(TRUE);
+      // Foreach field value we check whether we know the underlying property.
+      // If we exists we try to cast the value.
+      foreach ($field_item_value as $property_name => $property_value) {
+        if (isset($properties[$property_name]) && ($property = $field_item->get($property_name)) && $property instanceof PrimitiveInterface) {
+          $value[$delta][$property_name] = $property->getCastedValue();
+        }
+      }
+    }
+
+    return $value;
+  }
+
   /**
    * Responds to entity PATCH requests.
    *
@@ -221,7 +256,7 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
         }
 
         // Unchanged values for entity keys don't need access checking.
-        if ($original_entity->get($field_name)->getValue() === $entity->get($field_name)->getValue()) {
+        if ($this->getCastedValueFromFieldItemList($original_entity->get($field_name)) === $this->getCastedValueFromFieldItemList($entity->get($field_name))) {
           continue;
         }
         // It is not possible to set the language to NULL as it is automatically
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
index 2e332bdd72..ebb7dfac27 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
@@ -144,17 +144,17 @@ protected function getExpectedNormalizedEntity() {
       ],
       'status' => [
         [
-          'value' => 1,
+          'value' => TRUE,
         ],
       ],
       'created' => [
         [
-          'value' => '123456789',
+          'value' => 123456789,
         ],
       ],
       'changed' => [
         [
-          'value' => '123456789',
+          'value' => 123456789,
         ],
       ],
       'default_langcode' => [
@@ -164,7 +164,7 @@ protected function getExpectedNormalizedEntity() {
       ],
       'uid' => [
         [
-          'target_id' => $author->id(),
+          'target_id' => (int) $author->id(),
           'target_type' => 'user',
           'target_uuid' => $author->uuid(),
           'url' => base_path() . 'user/' . $author->id(),
@@ -178,7 +178,7 @@ protected function getExpectedNormalizedEntity() {
       ],
       'entity_id' => [
         [
-          'target_id' => '1',
+          'target_id' => 1,
           'target_type' => 'entity_test',
           'target_uuid' => EntityTest::load(1)->uuid(),
           'url' => base_path() . 'entity_test/1',
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 17b6cfdfa9..2139495fb3 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -371,11 +371,15 @@ public function testGet() {
     $this->assertEquals($this->getExpectedCacheTags(), empty($cache_tags_header_value) ? [] : explode(' ', $cache_tags_header_value));
     $cache_contexts_header_value = $response->getHeader('X-Drupal-Cache-Contexts')[0];
     $this->assertEquals($this->getExpectedCacheContexts(), empty($cache_contexts_header_value) ? [] : explode(' ', $cache_contexts_header_value));
-    // Comparing the exact serialization is pointless, because the order of
-    // fields does not matter (at least not yet). That's why we only compare the
-    // normalized entity with the decoded response: it's comparing PHP arrays
-    // instead of strings.
-    $this->assertEquals($this->getExpectedNormalizedEntity(), $this->serializer->decode((string) $response->getBody(), static::$format));
+    // Sort the serialization data first so we can do an identical comparison
+    // for the keys with the array order the same (it needs to match with
+    // identical comparison).
+    $expected = $this->getExpectedNormalizedEntity();
+    ksort($expected);
+    $actual = $this->serializer->decode((string) $response->getBody(), static::$format);
+    ksort($actual);
+    $this->assertSame($expected, $actual);
+
     // Not only assert the normalization, also assert deserialization of the
     // response results in the expected object.
     $unserialized = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format);
@@ -395,6 +399,36 @@ public function testGet() {
     $this->assertSame($get_headers, $head_headers);
 
 
+    // BC: serialization_update_8300().
+    // Only run this for fieldable entities. It doesn't make sense for config
+    // entities as config values are already casted. They also run through the
+    // ConfigEntityNormalizer, which doesn't deal with fields individually.
+    if ($this->entity instanceof FieldableEntityInterface) {
+      $this->config('serialization.settings')->set('bc_primitive_data_normalizer', TRUE)->save(TRUE);
+      // Rebuild the container so new config is reflected in the removal of the
+      // PrimitiveDataNormalizer.
+      $this->rebuildAll();
+
+
+      $response = $this->request('GET', $url, $request_options);
+      $this->assertResourceResponse(200, FALSE, $response);
+
+
+      // Again do an identical comparison, but this time transform the expected
+      // normalized entity's values to strings. This ensures the BC layer for
+      // bc_primitive_data_normalizer works as expected.
+      $expected = $this->getExpectedNormalizedEntity();
+      // Config entities are not affected.
+      // @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize()
+      $expected = static::castToString($expected);
+      ksort($expected);
+      $actual = $this->serializer->decode((string) $response->getBody(), static::$format);
+      ksort($actual);
+      $this->assertSame($expected, $actual);
+    }
+
+
+    // BC: rest_update_8203().
     $this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
     // @todo Remove this in https://www.drupal.org/node/2815845.
     drupal_flush_all_caches();
@@ -445,6 +479,32 @@ public function testGet() {
     $this->assertResourceErrorResponse(404, $message, $response);
   }
 
+  /**
+   * Transforms a normalization: casts all non-string types to strings.
+   *
+   * @param array $normalization
+   *   A normalization to transform.
+   *
+   * @return array
+   *   The transformed normalization.
+   *
+   * @see serialization_update_8300()
+   */
+  protected static function castToString(array $normalization) {
+    foreach ($normalization as $key => $value) {
+      if (is_bool($value)) {
+        $normalization[$key] = (string) (int) $value;
+      }
+      elseif (is_int($value) || is_float($value)) {
+        $normalization[$key] = (string) $value;
+      }
+      elseif (is_array($value)) {
+        $normalization[$key] = static::castToString($value);
+      }
+    }
+    return $normalization;
+  }
+
   /**
    * Tests a POST request for an entity, plus edge cases to ensure good DX.
    */
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
index da86d00060..e9621ea272 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestResourceTestBase.php
@@ -73,7 +73,7 @@ protected function getExpectedNormalizedEntity() {
       ],
       'id' => [
         [
-          'value' => '1',
+          'value' => 1,
         ],
       ],
       'langcode' => [
@@ -93,12 +93,12 @@ protected function getExpectedNormalizedEntity() {
       ],
       'created' => [
         [
-          'value' => $this->entity->get('created')->value,
+          'value' => (int) $this->entity->get('created')->value,
         ]
       ],
       'user_id' => [
         [
-          'target_id' => $author->id(),
+          'target_id' => (int) $author->id(),
           'target_type' => 'user',
           'target_uuid' => $author->uuid(),
           'url' => $author->toUrl()->toString(),
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
index d7651c4b90..1a4a3448b0 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Node/NodeResourceTestBase.php
@@ -116,32 +116,32 @@ protected function getExpectedNormalizedEntity() {
       ],
       'status' => [
         [
-          'value' => 1,
+          'value' => TRUE,
         ],
       ],
       'created' => [
         [
-          'value' => '123456789',
+          'value' => 123456789,
         ],
       ],
       'changed' => [
         [
-          'value' => '123456789',
+          'value' => 123456789,
         ],
       ],
       'promote' => [
         [
-          'value' => 1,
+          'value' => TRUE,
         ],
       ],
       'sticky' => [
         [
-          'value' => '0',
+          'value' => FALSE,
         ],
       ],
       'revision_timestamp' => [
         [
-          'value' => '123456789',
+          'value' => 123456789,
         ],
       ],
       'revision_translation_affected' => [
@@ -156,7 +156,7 @@ protected function getExpectedNormalizedEntity() {
       ],
       'uid' => [
         [
-          'target_id' => $author->id(),
+          'target_id' => (int) $author->id(),
           'target_type' => 'user',
           'target_uuid' => $author->uuid(),
           'url' => base_path() . 'user/' . $author->id(),
@@ -164,7 +164,7 @@ protected function getExpectedNormalizedEntity() {
       ],
       'revision_uid' => [
         [
-          'target_id' => $author->id(),
+          'target_id' => (int) $author->id(),
           'target_type' => 'user',
           'target_uuid' => $author->uuid(),
           'url' => base_path() . 'user/' . $author->id(),
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
index b6dce4f4b2..b0a369a433 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php
@@ -108,7 +108,7 @@ protected function getExpectedNormalizedEntity() {
       ],
       'changed' => [
         [
-          'value' => '123456789',
+          'value' => 123456789,
         ],
       ],
       'default_langcode' => [
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
index 38452c2a02..748bf58690 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/User/UserResourceTestBase.php
@@ -82,7 +82,7 @@ protected function createEntity() {
   protected function getExpectedNormalizedEntity() {
     return [
       'uid' => [
-        ['value' => '3'],
+        ['value' => 3],
       ],
       'uuid' => [
         ['value' => $this->entity->uuid()],
@@ -99,12 +99,12 @@ protected function getExpectedNormalizedEntity() {
       ],
       'created' => [
         [
-          'value' => '123456789',
+          'value' => 123456789,
         ],
       ],
       'changed' => [
         [
-          'value' => '123456789',
+          'value' => 123456789,
         ],
       ],
       'default_langcode' => [
diff --git a/core/modules/serialization/config/install/serialization.settings.yml b/core/modules/serialization/config/install/serialization.settings.yml
new file mode 100644
index 0000000000..90b3d948e4
--- /dev/null
+++ b/core/modules/serialization/config/install/serialization.settings.yml
@@ -0,0 +1,8 @@
+# Before Drupal 8.3, typed data primitive values were normalized as strings, as
+# this was usually returned from database storage. A primitive data normalizer
+# has been introduced to get the casted value instead. If this module is
+# installed the new behaviour will be the default. For modules updating this BC
+# option will be enabled.
+# @see serialization_update_8300()
+# @see https://www.drupal.org/node/2751325
+bc_primitive_data_normalizer: false
diff --git a/core/modules/serialization/config/schema/serialization.schema.yml b/core/modules/serialization/config/schema/serialization.schema.yml
new file mode 100644
index 0000000000..32321660b9
--- /dev/null
+++ b/core/modules/serialization/config/schema/serialization.schema.yml
@@ -0,0 +1,7 @@
+serialization.settings:
+  type: config_object
+  label: 'Serialization settings'
+  mapping:
+    bc_primitive_data_normalizer:
+      type: boolean
+      label: 'Whether the pre Drupal 8.3.x behavior of returning non-casted values from typed data (primitive) items.'
diff --git a/core/modules/serialization/serialization.install b/core/modules/serialization/serialization.install
new file mode 100644
index 0000000000..ebd0317c44
--- /dev/null
+++ b/core/modules/serialization/serialization.install
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the serialization module.
+ */
+
+/**
+ * @defgroup updates-8.2.x-to-8.3.x
+ * @{
+ * Update functions from 8.2.x to 8.3.x.
+ */
+
+/**
+ * Enable BC for non-casted values: continue to return string values.
+ */
+function serialization_update_8300() {
+  $config_factory = \Drupal::configFactory();
+
+  $config_factory->getEditable('serialization.settings')
+    ->set('bc_primitive_data_normalizer', TRUE)
+    ->save(TRUE);
+}
+
+/**
+ * @} End of "defgroup updates-8.2.x-to-8.3.x".
+ */
diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml
index c8fb3767f7..7bd2320914 100644
--- a/core/modules/serialization/serialization.services.yml
+++ b/core/modules/serialization/serialization.services.yml
@@ -17,6 +17,10 @@ services:
     tags:
       - { name: normalizer }
     arguments: ['@entity.manager']
+  serializer.normalizer.primitive_data:
+    class: Drupal\serialization\Normalizer\PrimitiveDataNormalizer
+    tags:
+      - { name: normalizer, priority: 5 }
   serializer.normalizer.complex_data:
     class: Drupal\serialization\Normalizer\ComplexDataNormalizer
     tags:
@@ -89,3 +93,9 @@ services:
     tags:
       - { name: event_subscriber }
     arguments: ['%serializer.formats%']
+    arguments: ['@serializer', '%serializer.formats%']
+  serialization.bc_config_subscriber:
+    class: Drupal\serialization\EventSubscriber\BcConfigSubscriber
+    tags:
+      - { name: event_subscriber }
+    arguments: ['@kernel']
diff --git a/core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php b/core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php
new file mode 100644
index 0000000000..98ff320c98
--- /dev/null
+++ b/core/modules/serialization/src/EventSubscriber/BcConfigSubscriber.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\serialization\EventSubscriber;
+
+use Drupal\Core\Config\ConfigCrudEvent;
+use Drupal\Core\Config\ConfigEvents;
+use Drupal\Core\DrupalKernelInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Config event subscriber to rebuild the container when BC config is saved.
+ */
+class BcConfigSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The Drupal Kernel.
+   *
+   * @var \Drupal\Core\DrupalKernelInterface
+   */
+  protected $kernel;
+
+  /**
+   * BcConfigSubscriber constructor.
+   *
+   * @param \Drupal\Core\DrupalKernelInterface $kernel
+   *   The Drupal Kernel.
+   */
+  public function __construct(DrupalKernelInterface $kernel) {
+    $this->kernel = $kernel;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[ConfigEvents::SAVE][] = 'onConfigSave';
+    return $events;
+  }
+
+  /**
+   * Invalidates the service container if serialization BC config gets updated.
+   *
+   * @param \Drupal\Core\Config\ConfigCrudEvent $event
+   */
+  public function onConfigSave(ConfigCrudEvent $event) {
+    $saved_config = $event->getConfig();
+
+    if ($saved_config->getName() === 'serialization.settings') {
+      if ($event->isChanged('bc_primitive_data_normalizer')) {
+        $this->kernel->invalidateContainer();
+      }
+    }
+  }
+
+}
diff --git a/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
index 5062738e9d..ef07d1185c 100644
--- a/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
+++ b/core/modules/serialization/src/Normalizer/ComplexDataNormalizer.php
@@ -26,6 +26,7 @@ class ComplexDataNormalizer extends NormalizerBase {
    */
   public function normalize($object, $format = NULL, array $context = array()) {
     $attributes = array();
+    /** @var \Drupal\Core\TypedData\TypedDataInterface $field */
     foreach ($object as $name => $field) {
       $attributes[$name] = $this->serializer->normalize($field, $format, $context);
     }
diff --git a/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php b/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php
new file mode 100644
index 0000000000..cce108cacf
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/PrimitiveDataNormalizer.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\serialization\Normalizer;
+
+use Drupal\Core\TypedData\PrimitiveInterface;
+
+/**
+ * Converts primitive data objects to their casted values.
+ */
+class PrimitiveDataNormalizer extends NormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var string
+   */
+  protected $supportedInterfaceOrClass = PrimitiveInterface::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = []) {
+    // Typed data casts NULL objects to their empty variants, so for example
+    // the empty string ('') for string type data, or 0 for integer typed data.
+    // In a better world with typed data implementing algebraic data types,
+    // getCastedValue would return NULL, but as typed data is not aware of real
+    // optional values on the primitive level, we implement our own optional
+    // value normalization here.
+    return $object->getValue() === NULL ? NULL : $object->getCastedValue();
+  }
+
+}
diff --git a/core/modules/serialization/src/RegisterSerializationClassesCompilerPass.php b/core/modules/serialization/src/RegisterSerializationClassesCompilerPass.php
index f4611f3fc1..ff8a43fbe8 100644
--- a/core/modules/serialization/src/RegisterSerializationClassesCompilerPass.php
+++ b/core/modules/serialization/src/RegisterSerializationClassesCompilerPass.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\serialization;
 
+use Drupal\Core\Config\BootstrapConfigStorageFactory;
 use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
@@ -22,6 +23,10 @@ public function process(ContainerBuilder $container) {
 
     // Retrieve registered Normalizers and Encoders from the container.
     foreach ($container->findTaggedServiceIds('normalizer') as $id => $attributes) {
+      if ($this->normalizerShouldBeSkipped($id)) {
+        continue;
+      }
+
       $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
       $normalizers[$priority][] = new Reference($id);
     }
@@ -53,6 +58,36 @@ public function process(ContainerBuilder $container) {
     $container->setParameter('serializer.format_providers', $format_providers);
   }
 
+  /**
+   * Determines whether a given normalizer should be skipped and not added.
+   *
+   * @param string $id
+   *   The normalizer ID.
+   *
+   * @return bool
+   */
+  protected function normalizerShouldBeSkipped($id) {
+    switch ($id) {
+      case 'serializer.normalizer.primitive_data':
+        return $this->normalizerBcSettingIsEnabled('bc_primitive_data_normalizer');
+      default:
+        // Default to FALSE as most normalizers will be added.
+        return FALSE;
+    }
+  }
+
+  /**
+   * Returns whether a normalizer BC setting is disabled or not.
+   *
+   * @param string $key
+   *
+   * @return bool
+   */
+  protected function normalizerBcSettingIsEnabled($key) {
+    $settings = BootstrapConfigStorageFactory::get()->read('serialization.settings');
+    return !empty($settings[$key]);
+  }
+
   /**
    * Sorts by priority.
    *
diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
index 8d7fdab6ef..d29f4937f0 100644
--- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
+++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php
@@ -110,7 +110,8 @@ public function testNormalize() {
       ),
       'user_id' => array(
         array(
-          'target_id' => $this->user->id(),
+          // id() will return the string value as it comes from the database.
+          'target_id' => (int) $this->user->id(),
           'target_type' => $this->user->getEntityTypeId(),
           'target_uuid' => $this->user->uuid(),
           'url' => $this->user->url(),
@@ -134,7 +135,7 @@ public function testNormalize() {
     $normalized = $this->serializer->normalize($this->entity);
 
     foreach (array_keys($expected) as $fieldName) {
-      $this->assertEqual($expected[$fieldName], $normalized[$fieldName], "ComplexDataNormalizer produces expected array for $fieldName.");
+      $this->assertSame($expected[$fieldName], $normalized[$fieldName], "Normalization produces expected array for $fieldName.");
     }
     $this->assertEqual(array_diff_key($normalized, $expected), array(), 'No unexpected data is added to the normalized array.');
   }
diff --git a/core/modules/serialization/tests/src/Unit/Normalizer/PrimitiveDataNormalizerTest.php b/core/modules/serialization/tests/src/Unit/Normalizer/PrimitiveDataNormalizerTest.php
new file mode 100644
index 0000000000..acdc6daa3a
--- /dev/null
+++ b/core/modules/serialization/tests/src/Unit/Normalizer/PrimitiveDataNormalizerTest.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Drupal\Tests\serialization\Unit\Normalizer;
+
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\TypedData\Plugin\DataType\BooleanData;
+use Drupal\Core\TypedData\Plugin\DataType\IntegerData;
+use Drupal\Core\TypedData\Plugin\DataType\StringData;
+use Drupal\Tests\UnitTestCase;
+use Drupal\serialization\Normalizer\PrimitiveDataNormalizer;
+
+/**
+ * @coversDefaultClass \Drupal\serialization\Normalizer\PrimitiveDataNormalizer
+ * @group serialization
+ */
+class PrimitiveDataNormalizerTest extends UnitTestCase {
+
+  /**
+   * The TypedDataNormalizer instance.
+   *
+   * @var \Drupal\serialization\Normalizer\TypedDataNormalizer
+   */
+  protected $normalizer;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->normalizer = new PrimitiveDataNormalizer();
+  }
+
+  /**
+   * @covers ::supportsNormalization
+   * @dataProvider dataProviderPrimitiveData
+   */
+  public function testSupportsNormalization($primitive_data, $expected) {
+    $this->assertTrue($this->normalizer->supportsNormalization($primitive_data));
+  }
+
+  /**
+   * @covers ::supportsNormalization
+   */
+  public function testSupportsNormalizationFail() {
+    // Test that an object not implementing PrimitiveInterface fails.
+    $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
+  }
+
+  /**
+   * @covers ::normalize
+   * @dataProvider dataProviderPrimitiveData
+   */
+  public function testNormalize($primitive_data, $expected) {
+    $this->assertSame($expected, $this->normalizer->normalize($primitive_data));
+  }
+
+  /**
+   * Data provider for testNormalize().
+   */
+  public function dataProviderPrimitiveData() {
+    $data = [];
+
+    $definition = DataDefinition::createFromDataType('string');
+    $string = new StringData($definition, 'string');
+    $string->setValue('test');
+
+    $data['string'] = [$string, 'test'];
+
+    $definition = DataDefinition::createFromDataType('string');
+    $string = new StringData($definition, 'string');
+    $string->setValue(NULL);
+
+    $data['string-null'] = [$string, NULL];
+
+    $definition = DataDefinition::createFromDataType('integer');
+    $integer = new IntegerData($definition, 'integer');
+    $integer->setValue(5);
+
+    $data['integer'] = [$integer, 5];
+
+    $definition = DataDefinition::createFromDataType('integer');
+    $integer = new IntegerData($definition, 'integer');
+    $integer->setValue(NULL);
+
+    $data['integer-null'] = [$integer, NULL];
+
+    $definition = DataDefinition::createFromDataType('boolean');
+    $boolean = new BooleanData($definition, 'boolean');
+    $boolean->setValue(TRUE);
+
+    $data['boolean'] = [$boolean, TRUE];
+
+    $definition = DataDefinition::createFromDataType('boolean');
+    $boolean = new BooleanData($definition, 'boolean');
+    $boolean->setValue(NULL);
+
+    $data['boolean-null'] = [$boolean, NULL];
+
+    return $data;
+  }
+
+}
