diff --git a/core/modules/jsonapi/jsonapi.services.yml b/core/modules/jsonapi/jsonapi.services.yml
index 16c7f4dc95..21f322d0ff 100644
--- a/core/modules/jsonapi/jsonapi.services.yml
+++ b/core/modules/jsonapi/jsonapi.services.yml
@@ -47,7 +47,10 @@ services:
     tags:
       - { name: jsonapi_normalizer }
   serializer.normalizer.content_entity.jsonapi:
-    class: Drupal\jsonapi\Normalizer\ContentEntityDenormalizer
+    alias: serializer.normalizer.fieldable_entity.jsonapi
+    deprecated: The "%alias_id%" service is deprecated. You should use the 'serializer.normalizer.fieldable_entity.jsonapi' service instead.
+  serializer.normalizer.fieldable_entity.jsonapi:
+    class: Drupal\jsonapi\Normalizer\FieldableEntityDenormalizer
     arguments: ['@entity_type.manager', '@entity_field.manager', '@plugin.manager.field.field_type']
     tags:
       - { name: jsonapi_normalizer }
diff --git a/core/modules/jsonapi/src/Controller/EntityResource.php b/core/modules/jsonapi/src/Controller/EntityResource.php
index 5c43d3ee24..ae169b46f0 100644
--- a/core/modules/jsonapi/src/Controller/EntityResource.php
+++ b/core/modules/jsonapi/src/Controller/EntityResource.php
@@ -7,7 +7,6 @@
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
-use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityRepositoryInterface;
@@ -1048,8 +1047,9 @@ protected function respondWithCollection(ResourceObjectData $primary_data, Data
    *   types.
    */
   protected function updateEntityField(ResourceType $resource_type, EntityInterface $origin, EntityInterface $destination, $field_name) {
-    // The update is different for configuration entities and content entities.
-    if ($origin instanceof ContentEntityInterface && $destination instanceof ContentEntityInterface) {
+    // The update is different for configuration entities and fieldable
+    // entities.
+    if ($origin instanceof FieldableEntityInterface && $destination instanceof FieldableEntityInterface) {
       // First scenario: both are content entities.
       $field_name = $resource_type->getInternalName($field_name);
       $destination_field_list = $destination->get($field_name);
diff --git a/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php b/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php
index 8a08cb3d4e..a3a4d9ae03 100644
--- a/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php
+++ b/core/modules/jsonapi/src/JsonApiResource/ResourceObject.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Entity\RevisionableInterface;
 use Drupal\Core\TypedData\TypedDataInternalPropertiesHelper;
 use Drupal\Core\Url;
@@ -211,10 +212,14 @@ public function toUrl() {
    *   entity, the fields will be scalar values or arrays.
    */
   protected static function extractFieldsFromEntity(ResourceType $resource_type, EntityInterface $entity) {
-    assert($entity instanceof ContentEntityInterface || $entity instanceof ConfigEntityInterface);
-    return $entity instanceof ContentEntityInterface
-      ? static::extractContentEntityFields($resource_type, $entity)
-      : static::extractConfigEntityFields($resource_type, $entity);
+    if ($entity instanceof FieldableEntityInterface) {
+      return static::extractFieldableEntityFields($resource_type, $entity);
+    }
+    elseif ($entity instanceof ConfigEntityInterface) {
+      return static::extractConfigEntityFields($resource_type, $entity);
+    }
+
+    return [];
   }

   /**
@@ -267,12 +272,32 @@ protected static function buildLinksFromEntity(ResourceType $resource_type, Enti
    * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
    *   The JSON:API resource type of the given entity.
    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
-   *   The config entity from which fields should be extracted.
+   *   The content entity from which fields should be extracted.
    *
    * @return \Drupal\Core\Field\FieldItemListInterface[]
    *   The fields extracted from a content entity.
+   *
+   * @deprecated in Drupal 8.8.x and will be removed before Drupal 9.0.0.
+   * Use \Drupal\jsonapi\JsonApiResource\ResourceObject::extractFieldableEntityFields()
+   * instead.
    */
   protected static function extractContentEntityFields(ResourceType $resource_type, ContentEntityInterface $entity) {
+    @trigger_error('\Drupal\jsonapi\JsonApiResource\ResourceObject::extractContentEntityFields() has been deprecated in favor of \Drupal\jsonapi\JsonApiResource\ResourceObject::extractFieldableEntityFields(). Use that instead.');
+    return static::extractFieldableEntityFields($resource_type, $entity);
+  }
+
+  /**
+   * Extracts a fieldable entity's fields.
+   *
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   The JSON:API resource type of the given entity.
+   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
+   *   The fieldable entity from which fields should be extracted.
+   *
+   * @return \Drupal\Core\Field\FieldItemListInterface[]
+   *   The fields extracted from a content entity.
+   */
+  protected static function extractFieldableEntityFields(ResourceType $resource_type, FieldableEntityInterface $entity) {
     $output = [];
     $fields = TypedDataInternalPropertiesHelper::getNonInternalProperties($entity->getTypedData());
     // Filter the array based on the field names.
diff --git a/core/modules/jsonapi/src/Normalizer/ContentEntityDenormalizer.php b/core/modules/jsonapi/src/Normalizer/ContentEntityDenormalizer.php
index cd821d4975..19e795a19f 100644
--- a/core/modules/jsonapi/src/Normalizer/ContentEntityDenormalizer.php
+++ b/core/modules/jsonapi/src/Normalizer/ContentEntityDenormalizer.php
@@ -2,6 +2,8 @@

 namespace Drupal\jsonapi\Normalizer;

+@trigger_error('\Drupal\jsonapi\Normalizer\ContentEntityDenormalizer has been deprecated in favor of \Drupal\jsonapi\Normalizer\FieldableEntityDenormalizer. Use that instead.');
+
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
diff --git a/core/modules/jsonapi/src/Normalizer/FieldableEntityDenormalizer.php b/core/modules/jsonapi/src/Normalizer/FieldableEntityDenormalizer.php
new file mode 100644
index 0000000000..ec738cb9c7
--- /dev/null
+++ b/core/modules/jsonapi/src/Normalizer/FieldableEntityDenormalizer.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\jsonapi\Normalizer;
+
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\jsonapi\ResourceType\ResourceType;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+
+/**
+ * Converts a JSON:API array structure into a Drupal entity object.
+ *
+ * @internal JSON:API maintains no PHP API since its API is the HTTP API. This
+ *   class may change at any time and this will break any dependencies on it.
+ *
+ * @see https://www.drupal.org/project/jsonapi/issues/3032787
+ * @see jsonapi.api.php
+ */
+final class FieldableEntityDenormalizer extends EntityDenormalizerBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = FieldableEntityInterface::class;
+
+  /**
+   * Prepares the input data to create the entity.
+   *
+   * @param array $data
+   *   The input data to modify.
+   * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
+   *   Contains the info about the resource type.
+   * @param string $format
+   *   Format the given data was extracted from.
+   * @param array $context
+   *   Options available to the denormalizer.
+   *
+   * @return array
+   *   The modified input data.
+   */
+  protected function prepareInput(array $data, ResourceType $resource_type, $format, array $context) {
+    $data_internal = [];
+
+    $field_map = $this->fieldManager->getFieldMap()[$resource_type->getEntityTypeId()];
+
+    $entity_type_id = $resource_type->getEntityTypeId();
+    $entity_type_definition = $this->entityTypeManager->getDefinition($entity_type_id);
+    $bundle_key = $entity_type_definition->getKey('bundle');
+    $uuid_key = $entity_type_definition->getKey('uuid');
+
+    // Translate the public fields into the entity fields.
+    foreach ($data as $public_field_name => $field_value) {
+      $internal_name = $resource_type->getInternalName($public_field_name);
+
+      // Skip any disabled field, except the always required bundle key and
+      // required-in-case-of-PATCHing uuid key.
+      // @see \Drupal\jsonapi\ResourceType\ResourceTypeRepository::getFieldMapping()
+      if ($resource_type->hasField($internal_name) && !$resource_type->isFieldEnabled($internal_name) && $bundle_key !== $internal_name && $uuid_key !== $internal_name) {
+        continue;
+      }
+
+      if (!isset($field_map[$internal_name]) || !in_array($resource_type->getBundle(), $field_map[$internal_name]['bundles'], TRUE)) {
+        throw new UnprocessableEntityHttpException(sprintf(
+          'The attribute %s does not exist on the %s resource type.',
+          $internal_name,
+          $resource_type->getTypeName()
+        ));
+      }
+
+      $field_type = $field_map[$internal_name]['type'];
+      $field_class = $this->pluginManager->getDefinition($field_type)['list_class'];
+
+      $field_denormalization_context = array_merge($context, [
+        'field_type' => $field_type,
+        'field_name' => $internal_name,
+        'field_definition' => $this->fieldManager->getFieldDefinitions($resource_type->getEntityTypeId(), $resource_type->getBundle())[$internal_name],
+      ]);
+      $data_internal[$internal_name] = $this->serializer->denormalize($field_value, $field_class, $format, $field_denormalization_context);
+    }
+
+    return $data_internal;
+  }
+
+}
diff --git a/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
index 995dfeed42..38b6b6b5bb 100644
--- a/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
+++ b/core/modules/jsonapi/src/ResourceType/ResourceTypeRepository.php
@@ -5,9 +5,9 @@
 use Drupal\Component\Assertion\Inspector;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
 use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
 use Drupal\Core\Entity\ContentEntityNullStorage;
-use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
@@ -195,7 +195,6 @@ public function getByTypeName($type_name) {
    */
   protected static function getFieldMapping(array $field_names, EntityTypeInterface $entity_type, $bundle) {
     assert(Inspector::assertAllStrings($field_names));
-    assert($entity_type instanceof ContentEntityTypeInterface || $entity_type instanceof ConfigEntityTypeInterface);
     assert(is_string($bundle) && !empty($bundle), 'A bundle ID is required. Bundleless entity types should pass the entity type ID again.');

     $mapping = [];
@@ -263,14 +262,14 @@ protected static function getFieldMapping(array $field_names, EntityTypeInterfac
    *   All field names.
    */
   protected function getAllFieldNames(EntityTypeInterface $entity_type, $bundle) {
-    if ($entity_type instanceof ContentEntityTypeInterface) {
+    if (is_a($entity_type->getClass(), FieldableEntityInterface::class, TRUE)) {
       $field_definitions = $this->entityFieldManager->getFieldDefinitions(
         $entity_type->id(),
         $bundle
       );
       return array_keys($field_definitions);
     }
-    elseif ($entity_type instanceof ConfigEntityTypeInterface) {
+    elseif (is_a($entity_type->getClass(), ConfigEntityInterface::class, TRUE)) {
       // @todo Uncomment the first line, remove everything else once https://www.drupal.org/project/drupal/issues/2483407 lands.
       // return array_keys($entity_type->getPropertiesToExport());
       $export_properties = $entity_type->getPropertiesToExport();
@@ -281,9 +280,8 @@ protected function getAllFieldNames(EntityTypeInterface $entity_type, $bundle) {
         return ['id', 'type', 'uuid', '_core'];
       }
     }
-    else {
-      throw new \LogicException("Only content and config entity types are supported.");
-    }
+
+    return [];
   }

   /**
