 .../src/Normalizer/NormalizerBase.php              | 53 ++++++++++++++++++++++
 .../src/Kernel/NoFieldTypeNormalizersTest.php      | 49 ++++++++++++++++++++
 2 files changed, 102 insertions(+)

diff --git a/core/modules/serialization/src/Normalizer/NormalizerBase.php b/core/modules/serialization/src/Normalizer/NormalizerBase.php
index 958aaf2..3f4e482 100644
--- a/core/modules/serialization/src/Normalizer/NormalizerBase.php
+++ b/core/modules/serialization/src/Normalizer/NormalizerBase.php
@@ -3,6 +3,13 @@
 namespace Drupal\serialization\Normalizer;
 
 use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Field\FieldItemInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\field_normalization_test\Normalization\TextItemSillyNormalizer;
+use Drupal\hal\Normalizer\FieldNormalizer as HalFieldNormalizer;
+use Drupal\hal\Normalizer\FieldItemNormalizer as HalFieldItemNormalizer;
+use Drupal\hal\Normalizer\EntityReferenceItemNormalizer as HalEntityReferenceItemNormalizer;
+use Drupal\hal\Normalizer\TimestampItemNormalizer as HalTimestampItemNormalizer;
 use Drupal\rest\EventSubscriber\ResourceResponseSubscriber;
 use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 use Symfony\Component\Serializer\Normalizer\SerializerAwareNormalizer;
@@ -37,6 +44,7 @@ public function supportsNormalization($data, $format = NULL) {
     }
 
     $supported = (array) $this->supportedInterfaceOrClass;
+    static::disallowFieldLevelNormalizers($supported, get_class($this));
 
     return (bool) array_filter($supported, function ($name) use ($data) {
       return $data instanceof $name;
@@ -57,6 +65,7 @@ public function supportsDenormalization($data, $type, $format = NULL) {
     }
 
     $supported = (array) $this->supportedInterfaceOrClass;
+    static::disallowFieldLevelNormalizers($supported, get_class($this));
 
     $subclass_check = function ($name) use ($type) {
       return (class_exists($name) || interface_exists($name)) && is_subclass_of($type, $name, TRUE);
@@ -97,4 +106,48 @@ protected function addCacheableDependency(array $context, $data) {
     }
   }
 
+  /**
+   * Triggers deprecation error for @FieldType-level normalizers, gives advice.
+   *
+   * @param array $supported
+   *   The interface(s) or class(es) that this normalizer supports.
+   * @param $normalizer_class
+   *   The class that implements this normalizer.
+   *
+   * @internal
+   */
+  protected static function disallowFieldLevelNormalizers(array $supported, $normalizer_class) {
+    $allowed_exceptions = [
+      // Generic field normalizers for both (default and 'hal') normalizations.
+      FieldNormalizer::class,
+      FieldItemNormalizer::class,
+      HalFieldNormalizer::class,
+      HalFieldItemNormalizer::class,
+      // Entity reference field normalizers for both normalizations.
+      EntityReferenceFieldItemNormalizer::class,
+      HalEntityReferenceItemNormalizer::class,
+      // The NULL normalizer allows one to prevent listed classes from ever
+      // being normalized. By default, this is applied only  tothe 'password'
+      // field type.
+      NullNormalizer::class,
+      // TextItemSillyNormalizer is a test-only normalizer.
+      TextItemSillyNormalizer::class,
+      // TimestampItemNormalizer is being deprecated in https://www.drupal.org/project/drupal/issues/2926508.
+      TimestampItemNormalizer::class,
+      HalTimestampItemNormalizer::class,
+    ];
+
+    $is_disallowed = function ($name) {
+      return in_array($name, [FieldItemInterface::class, FieldItemListInterface::class], TRUE)
+        || is_subclass_of($name, FieldItemInterface::class, TRUE)
+        || is_subclass_of($name, FieldItemListInterface::class, TRUE);
+    };
+
+    array_walk($supported, function ($name) use ($normalizer_class, $allowed_exceptions, $is_disallowed) {
+      if (!in_array($normalizer_class, $allowed_exceptions, TRUE) && $is_disallowed($name)) {
+        @trigger_error(sprintf('%s is a normalizer for a @FieldType plugin. This is very strongly discouraged because it requires similar logic to be implemented for every normalization. Please write this as a normalizer for a @DataType plugin instead, then it works for the entire Drupal API-First ecosystem!', $normalizer_class), E_USER_DEPRECATED);
+      }
+    });
+  }
+
 }
diff --git a/core/modules/serialization/tests/src/Kernel/NoFieldTypeNormalizersTest.php b/core/modules/serialization/tests/src/Kernel/NoFieldTypeNormalizersTest.php
new file mode 100644
index 0000000..e65a841
--- /dev/null
+++ b/core/modules/serialization/tests/src/Kernel/NoFieldTypeNormalizersTest.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\Tests\serialization\Kernel;
+
+use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\KernelTests\KernelTestBase;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Tests that no non-deprecated normalizer services target field types.
+ *
+ * @see \Drupal\serialization\Normalizer\NormalizerBase::disallowFieldLevelNormalizers()
+ *
+ * @group serialization
+ */
+class NoFieldTypeNormalizersTest extends KernelTestBase {
+
+  /**
+   * Tests that no non-deprecated normalizer services target field types.
+   */
+  public function testEntityTypeRestTestCoverage() {
+    // Enable all modules.
+    $listing = new ExtensionDiscovery(\Drupal::root());
+    $stable_core_modules = $listing->scan('module');
+    $this->enableModules(array_keys($stable_core_modules));
+
+    $normalizer_service_ids = array_keys($this->container->findTaggedServiceIds('normalizer'));
+    foreach ($normalizer_service_ids as $service_id) {
+      // Violating normalizers that have been deprecated can be ignored.
+      if ($this->container->getDefinition($service_id)->isDeprecated()) {
+        continue;
+      }
+
+      $normalizer = $this->container->get($service_id);
+
+      // Calling ::supportsNormalization() or ::supportsDenormalization() will
+      // cause NormalizerBase::disallowFieldLevelNormalizers() to be called,
+      // which would trigger the E_USER_DEPRECATED error.
+      if ($normalizer instanceof NormalizerInterface) {
+        $normalizer->supportsNormalization(new \stdClass());
+      }
+      if ($normalizer instanceof DenormalizerInterface) {
+        $normalizer->supportsDenormalization(TRUE, $this->randomMachineName());
+      }
+    }
+  }
+
+}
