diff --git a/jsonapi_extras.services.yml b/jsonapi_extras.services.yml
index 4dcd03f9c091322ae02bf3a5a438d825db1d1e16..101c16b57539eb6f24f269d5128873fa27c37d27 100644
--- a/jsonapi_extras.services.yml
+++ b/jsonapi_extras.services.yml
@@ -11,21 +11,27 @@ services:
   serializer.normalizer.entity_reference_item.jsonapi_extras:
     class: Drupal\jsonapi_extras\Normalizer\RelationshipItemNormalizer
     arguments:
-      - '@serializer.normalizer.entity_reference_item.jsonapi'
+      - '@serializer.normalizer.resource_identifier.jsonapi'
       - '@jsonapi.resource_type.repository'
     tags:
       - { name: normalizer, priority: 1025, format: api_json }
 
-  serializer.normalizer.entity.jsonapi_extras:
-    class: Drupal\jsonapi_extras\Normalizer\ContentEntityNormalizer
-    parent: serializer.normalizer.entity.jsonapi
-    calls:
-      - [setSerializer, ['@jsonapi.serializer_do_not_use_removal_imminent']]
+  serializer.normalizer.resource_object.jsonapi_extras:
+    class: Drupal\jsonapi_extras\Normalizer\ResourceObjectNormalizer
+    decorates: serializer.normalizer.resource_object.jsonapi
+    arguments: ['@serializer.normalizer.resource_object.jsonapi_extras.inner']
+    tags:
+      - { name: normalizer, 1022, format: api_json }
+  serializer.normalizer.content_entity.jsonapi_extras:
+    class: Drupal\jsonapi_extras\Normalizer\ContentEntityDenormalizer
+    decorates: serializer.normalizer.content_entity.jsonapi
+    arguments: ['@serializer.normalizer.content_entity.jsonapi_extras.inner']
     tags:
       - { name: normalizer, priority: 1022, format: api_json }
   serializer.normalizer.config_entity.jsonapi_extras:
-    class: Drupal\jsonapi_extras\Normalizer\ConfigEntityNormalizer
-    parent: serializer.normalizer.config_entity.jsonapi
+    class: Drupal\jsonapi_extras\Normalizer\ConfigEntityDenormalizer
+    decorates: serializer.normalizer.config_entity.jsonapi
+    arguments: ['@serializer.normalizer.config_entity.jsonapi_extras.inner']
     tags:
       - { name: normalizer, priority: 1022, format: api_json }
 
@@ -42,10 +48,10 @@ services:
 
   jsonapi_extras.entity.to_jsonapi:
     class: Drupal\jsonapi_extras\EntityToJsonApi
-    arguments: ['@jsonapi.serializer_do_not_use_removal_imminent', '@jsonapi.resource_type.repository', '@current_user', '@request_stack', '%jsonapi.base_path%']
+    arguments: ['@jsonapi.serializer', '@jsonapi.resource_type.repository', '@current_user', '@jsonapi.include_resolver']
 
-  jsonapi.serializer_do_not_use_removal_imminent.jsonapi_extras:
+  jsonapi.serializer.jsonapi_extras:
     class: \Drupal\jsonapi_extras\SerializerDecorator
     public: false
-    decorates: jsonapi.serializer_do_not_use_removal_imminent
-    arguments: ['@jsonapi.serializer_do_not_use_removal_imminent.jsonapi_extras.inner']
+    decorates: jsonapi.serializer
+    arguments: ['@jsonapi.serializer.jsonapi_extras.inner']
diff --git a/modules/jsonapi_defaults/src/Controller/EntityResource.php b/modules/jsonapi_defaults/src/Controller/EntityResource.php
index 31a3da2ceb89b307a6d590538f4fb0c1c52b007b..a60778d3e5f08c3e525d993446966344fa4f2bb2 100644
--- a/modules/jsonapi_defaults/src/Controller/EntityResource.php
+++ b/modules/jsonapi_defaults/src/Controller/EntityResource.php
@@ -15,6 +15,56 @@ use Symfony\Component\HttpFoundation\Request;
  */
 class EntityResource extends JsonApiEntityResourse {
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function getJsonApiParams(Request $request, ResourceType $resource_type) {
+    // If this is a related resource, then we need to swap to the new resource
+    // type.
+    $related_field = $request->attributes->get('_on_relationship')
+      ? NULL
+      : $request->attributes->get('related');
+    try {
+      $resource_type = static::correctResourceTypeOnRelated($related_field, $resource_type);
+    }
+    catch (\LengthException $e) {
+      watchdog_exception('jsonapi_defaults', $e);
+      $resource_type = NULL;
+    }
+
+    if (!$resource_type instanceof ConfigurableResourceType) {
+      return parent::getJsonApiParams($request, $resource_type);
+    }
+    $resource_config = $resource_type->getJsonapiResourceConfig();
+    if (!$resource_config instanceof JsonapiResourceConfig) {
+      return parent::getJsonApiParams($request, $resource_type);
+    }
+    $default_filter_input = $resource_config->getThirdPartySetting(
+      'jsonapi_defaults',
+      'default_filter',
+      []
+    );
+
+    $default_filter = [];
+    foreach ($default_filter_input as $key => $value) {
+      if (substr($key, 0, 6) === 'filter') {
+        $key = str_replace('filter:', '', $key);
+        // TODO: Replace this with use of the NestedArray utility.
+        $this->setFilterValue($default_filter, $key, $value);
+      }
+    }
+    $filters = array_merge(
+      $default_filter,
+      $request->query->get('filter', [])
+    );
+
+    if (!empty($filters)) {
+      $request->query->set('filter', $filters);
+    }
+
+    return parent::getJsonApiParams($request, $resource_type);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -93,4 +143,24 @@ class EntityResource extends JsonApiEntityResourse {
     return $relatable_resource_types[0];
   }
 
+  /**
+   * Set filter into nested array.
+   *
+   * @param array $arr
+   *   The default filter.
+   * @param string $path
+   *   The filter path.
+   * @param mixed $value
+   *   The filter value.
+   */
+  private function setFilterValue(array &$arr, $path, $value) {
+    $keys = explode('#', $path);
+
+    foreach ($keys as $key) {
+      $arr = &$arr[$key];
+    }
+
+    $arr = $value;
+  }
+
 }
diff --git a/modules/jsonapi_defaults/src/JsonApiDefaultsJsonApiParamEnhancer.php b/modules/jsonapi_defaults/src/JsonApiDefaultsJsonApiParamEnhancer.php
deleted file mode 100644
index 175c3852076ebd0561c13a25ff2ff86979044413..0000000000000000000000000000000000000000
--- a/modules/jsonapi_defaults/src/JsonApiDefaultsJsonApiParamEnhancer.php
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-namespace Drupal\jsonapi_defaults;
-
-use Drupal\jsonapi\Routing\JsonApiParamEnhancer;
-use Drupal\jsonapi\Routing\Routes;
-use Drupal\jsonapi_defaults\Controller\EntityResource;
-use Drupal\jsonapi_extras\Entity\JsonapiResourceConfig;
-use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType;
-use Symfony\Cmf\Component\Routing\RouteObjectInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\Request;
-
-/**
- * JsonApiDefaultsJsonApiParamEnhancer class.
- *
- * @internal
- */
-class JsonApiDefaultsJsonApiParamEnhancer extends JsonApiParamEnhancer {
-
-  /**
-   * Configuration manager.
-   *
-   * @var \Drupal\Core\Config\ConfigManagerInterface
-   */
-  protected $configManager;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setContainer(ContainerInterface $container = NULL) {
-    parent::setContainer($container);
-
-    $this->configManager = $container->get('config.manager');
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function enhance(array $defaults, Request $request) {
-    if (!Routes::isJsonApiRequest($defaults)) {
-      return parent::enhance($defaults, $request);
-    }
-    $resource_type = Routes::getResourceTypeNameFromParameters($defaults);
-    // If this is a related resource, then we need to swap to the new resource
-    // type.
-    $route = $defaults[RouteObjectInterface::ROUTE_OBJECT];
-    $related_field = $route->getDefault('_on_relationship')
-      ? NULL
-      : $route->getDefault('related');
-    try {
-      $resource_type = EntityResource::correctResourceTypeOnRelated($related_field, $resource_type);
-    }
-    catch (\LengthException $e) {
-      watchdog_exception('jsonapi_defaults', $e);
-      $resource_type = NULL;
-    }
-
-    if (!$resource_type instanceof ConfigurableResourceType) {
-      return parent::enhance($defaults, $request);
-    }
-    $resource_config = $resource_type->getJsonapiResourceConfig();
-    if (!$resource_config instanceof JsonapiResourceConfig) {
-      return parent::enhance($defaults, $request);
-    }
-    $default_filter_input = $resource_config->getThirdPartySetting(
-      'jsonapi_defaults',
-      'default_filter',
-      []
-    );
-
-    $default_filter = [];
-    foreach ($default_filter_input as $key => $value) {
-      if (substr($key, 0, 6) === 'filter') {
-        $key = str_replace('filter:', '', $key);
-        // TODO: Replace this with use of the NestedArray utility.
-        $this->setFilterValue($default_filter, $key, $value);
-      }
-    }
-    $filters = array_merge(
-      $default_filter,
-      $request->query->get('filter', [])
-    );
-
-    if (!empty($filters)) {
-      $request->query->set('filter', $filters);
-    }
-
-    return parent::enhance($defaults, $request);
-  }
-
-  /**
-   * Set filter into nested array.
-   *
-   * @param array $arr
-   *   The default filter.
-   * @param string $path
-   *   The filter path.
-   * @param mixed $value
-   *   The filter value.
-   */
-  private function setFilterValue(array &$arr, $path, $value) {
-    $keys = explode('#', $path);
-
-    foreach ($keys as $key) {
-      $arr = &$arr[$key];
-    }
-
-    $arr = $value;
-  }
-
-}
diff --git a/modules/jsonapi_defaults/src/JsonapiDefaultsServiceProvider.php b/modules/jsonapi_defaults/src/JsonapiDefaultsServiceProvider.php
index 7ff2060dc2e99fd997479e21522163cc5b812ef6..60da4d7607a8594cdcf0f665aac1b16a4dfc2f3c 100644
--- a/modules/jsonapi_defaults/src/JsonapiDefaultsServiceProvider.php
+++ b/modules/jsonapi_defaults/src/JsonapiDefaultsServiceProvider.php
@@ -16,11 +16,6 @@ class JsonapiDefaultsServiceProvider extends ServiceProviderBase {
   public function alter(ContainerBuilder $container) {
     /** @var \Symfony\Component\DependencyInjection\Definition $definition */
 
-    if ($container->hasDefinition('jsonapi.params.enhancer')) {
-      $definition = $container->getDefinition('jsonapi.params.enhancer');
-      $definition->setClass('Drupal\jsonapi_defaults\JsonApiDefaultsJsonApiParamEnhancer');
-    }
-
     if ($container->hasDefinition('jsonapi.entity_resource')) {
       $definition = $container->getDefinition('jsonapi.entity_resource');
       $definition->setClass('Drupal\jsonapi_defaults\Controller\EntityResource');
diff --git a/src/EntityToJsonApi.php b/src/EntityToJsonApi.php
index 236e80442bfdc42cfdc84172bb1aba9922fc4a34..bcea94589d0c6a322dc65255f4e19ca3e0b6254a 100644
--- a/src/EntityToJsonApi.php
+++ b/src/EntityToJsonApi.php
@@ -4,14 +4,13 @@ namespace Drupal\jsonapi_extras;
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\jsonapi\JsonApiResource\EntityCollection;
+use Drupal\jsonapi\IncludeResolver;
 use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel;
 use Drupal\jsonapi\JsonApiResource\LinkCollection;
+use Drupal\jsonapi\JsonApiResource\NullEntityCollection;
+use Drupal\jsonapi\JsonApiResource\ResourceObject;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
-use Drupal\jsonapi\Routing\Routes;
 use Drupal\jsonapi\Serializer\Serializer;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\RequestStack;
 use Symfony\Component\Serializer\SerializerInterface;
 
 /**
@@ -43,18 +42,11 @@ class EntityToJsonApi {
   protected $resourceTypeRepository;
 
   /**
-   * The master request.
+   * The JSON:API include resolver.
    *
-   * @var \Symfony\Component\HttpFoundation\Request
+   * @var \Drupal\jsonapi\IncludeResolver
    */
-  protected $masterRequest;
-
-  /**
-   * The JSON:API base path.
-   *
-   * @var string
-   */
-  protected $jsonApiBasePath;
+  protected $includeResolver;
 
   /**
    * EntityToJsonApi constructor.
@@ -65,22 +57,15 @@ class EntityToJsonApi {
    *   The resource type repository.
    * @param \Drupal\Core\Session\AccountInterface $current_user
    *   The currently logged in user.
-   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
-   *   The request stack.
-   * @param string $jsonapi_base_path
-   *   The JSON:API base path.
+   * @param \Drupal\jsonapi\IncludeResolver $include_resolver
+   *   The JSON:API include resolver.
    */
-  public function __construct(SerializerInterface $serializer, ResourceTypeRepositoryInterface $resource_type_repository, AccountInterface $current_user, RequestStack $request_stack, $jsonapi_base_path) {
+  public function __construct(SerializerInterface $serializer, ResourceTypeRepositoryInterface $resource_type_repository, AccountInterface $current_user, IncludeResolver $include_resolver) {
     assert($serializer instanceof Serializer || $serializer instanceof SerializerDecorator);
     $this->serializer = $serializer;
     $this->resourceTypeRepository = $resource_type_repository;
     $this->currentUser = $current_user;
-    $this->masterRequest = $request_stack->getMasterRequest();
-    assert(is_string($jsonapi_base_path));
-    assert($jsonapi_base_path[0] === '/');
-    assert(isset($jsonapi_base_path[1]));
-    assert(substr($jsonapi_base_path, -1) !== '/');
-    $this->jsonApiBasePath = $jsonapi_base_path;
+    $this->includeResolver = $include_resolver;
   }
 
   /**
@@ -95,12 +80,7 @@ class EntityToJsonApi {
    *   The raw JSON string of the requested resource.
    */
   public function serialize(EntityInterface $entity, array $includes = []) {
-    $context = $this->calculateContext($entity, $includes);
-    $document = $this->prepareDocument($entity, $includes, $context);
-    return $this->serializer->serialize($document,
-      'api_json',
-      $context
-    );
+    return $this->serializer->encode($this->normalize($entity, $includes), 'api_json');
   }
 
   /**
@@ -115,8 +95,10 @@ class EntityToJsonApi {
    *   The JSON structure of the requested resource.
    */
   public function normalize(EntityInterface $entity, array $includes = []) {
-    $context = $this->calculateContext($entity, $includes);
-    $document = $this->prepareDocument($entity, $includes, $context);
+    $resource_type = $this->resourceTypeRepository->get($entity->getEntityTypeId(), $entity->bundle());
+    $resource_object = new ResourceObject($resource_type, $entity);
+    $context = $this->calculateContext($resource_object);
+    $document = $this->prepareDocument($resource_object, $includes);
     return $this->serializer->normalize($document,
       'api_json',
       $context
@@ -126,82 +108,35 @@ class EntityToJsonApi {
   /**
    * Calculate the arguments for the serialize/normalize operation.
    *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The entity to generate the JSON from.
-   * @param string[] $includes
-   *   The list of includes.
+   * @param \Drupal\jsonapi\JsonApiResource\ResourceObject $resource_object
+   *   The resource object to generate the JSON from.
    *
    * @return array
    *   The list of arguments for serialize/normalize operation.
    */
-  protected function calculateContext(
-    EntityInterface $entity,
-    array $includes = []
-  ) {
-    $entity_type_id = $entity->getEntityTypeId();
-    $resource_type = $this->resourceTypeRepository->get(
-      $entity_type_id,
-      $entity->bundle()
-    );
-    // The overridden resource type implementation of "jsonapi_extras" may
-    // return a value containing a leading slash. Since this was initial
-    // behavior we won't going to break the things and ready to tackle both
-    // cases: with or without a leading slash.
-    $resource_path = ltrim($resource_type->getPath(), '/');
-    $path = sprintf(
-      '%s/%s/%s',
-      rtrim($this->jsonApiBasePath, '/'),
-      rtrim($resource_path, '/'),
-      $entity->uuid()
-    );
-    $request = Request::create($this->masterRequest->getUriForPath($path));
-
-    // We don't have to filter the "$include" since this will be done later.
-    // @see JsonApiDocumentTopLevelNormalizer::expandContext()
-    $request->query->set('include', implode(',', $includes));
-    $request->attributes->set($entity_type_id, $entity);
-    $request->attributes->set(Routes::RESOURCE_TYPE_KEY, $resource_type);
-    $request->attributes->set(Routes::JSON_API_ROUTE_FLAG_KEY, TRUE);
-
+  protected function calculateContext(ResourceObject $resource_object) {
     return [
       'account' => $this->currentUser,
-      'resource_type' => $resource_type,
-      'request' => $request,
+      'resource_object' => $resource_object,
     ];
   }
 
   /**
    * Prepare the JsonApi Document for serialization or normalization.
    *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
-   *   The entity to prepare the document from.
+   * @param \Drupal\jsonapi\JsonApiResource\ResourceObject $resource_object
+   *   The resource object to generate the JSON from.
    * @param array $includes
    *   An array of included related entities to add to the document.
-   * @param array $context
-   *   The prepared context.
    *
    * @return \Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel
    *   The top level document.
    */
-  private function prepareDocument(EntityInterface $entity, array $includes, array $context) {
-    /** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type */
-    $resource_type = $context['resource_type'];
-    $referenced_entities = [];
-    foreach ($includes as $public_name) {
-      // Ensure we are getting the proper field names of includes regardless of
-      // whether the public or internal name was passed in.
-      $internal_name = $resource_type->getInternalName($public_name);
-      $referenced_entities = array_merge(
-        $referenced_entities,
-        $entity->get($internal_name)->referencedEntities()
-      );
-    }
-    $entity_collection = new EntityCollection($referenced_entities);
-    return new JsonApiDocumentTopLevel(
-      $entity,
-      $entity_collection,
-      new LinkCollection([])
-    );
+  private function prepareDocument(ResourceObject $resource_object, array $includes) {
+    $includes = !empty($includes)
+      ? $this->includeResolver->resolve($resource_object, implode(',', $includes))
+      : new NullEntityCollection();
+    return new JsonApiDocumentTopLevel($resource_object, $includes, new LinkCollection([]));
   }
 
 }
diff --git a/src/JsonapiExtrasServiceProvider.php b/src/JsonapiExtrasServiceProvider.php
index e7b35d387b6f93aa52ce6c85877fe16ea6692be8..6535285f3994ae08b3e73c9aed1b3922cb4897a6 100644
--- a/src/JsonapiExtrasServiceProvider.php
+++ b/src/JsonapiExtrasServiceProvider.php
@@ -34,19 +34,23 @@ class JsonapiExtrasServiceProvider extends ServiceProviderBase {
     if ($container->has('serializer.normalizer.field_item.jsonapi')) {
       $container->getDefinition('serializer.normalizer.field_item.jsonapi')->setPrivate(TRUE)->clearTags();
     }
-    if ($container->has('serializer.normalizer.entity_reference_item.jsonapi')) {
-      $container->getDefinition('serializer.normalizer.entity_reference_item.jsonapi')->setPrivate(TRUE)->clearTags();
+    if ($container->has('serializer.normalizer.resource_identifier.jsonapi')) {
+      $container->getDefinition('serializer.normalizer.resource_identifier.jsonapi')->setPrivate(TRUE)->clearTags();
     }
-    if ($container->has('serializer.normalizer.entity.jsonapi')) {
-      $container->getDefinition('serializer.normalizer.entity.jsonapi')->setPrivate(TRUE)->clearTags();
+    if ($container->has('serializer.normalizer.resource_object.jsonapi')) {
+      $container->getDefinition('serializer.normalizer.resource_object.jsonapi')->clearTags()->addMethodCall('setSerializer', [new Reference('jsonapi.serializer')]);
+    }
+    if ($container->has('serializer.normalizer.content_entity.jsonapi')) {
+      $definition = $container->getDefinition('serializer.normalizer.content_entity.jsonapi');
+      $definition->clearTags()->addMethodCall('setSerializer', [new Reference('jsonapi.serializer')]);
     }
     if ($container->has('serializer.normalizer.config_entity.jsonapi')) {
-      $container->getDefinition('serializer.normalizer.config_entity.jsonapi')->setPrivate(TRUE)->clearTags();
+      $container->getDefinition('serializer.normalizer.config_entity.jsonapi')->clearTags()->addMethodCall('setSerializer', [new Reference('jsonapi.serializer')]);
     }
 
     // Break a circular dependency.
     // @see \Drupal\jsonapi_extras\SerializerDecorator::lazilyInitialize()
-    $definition = $container->getDefinition('jsonapi.serializer_do_not_use_removal_imminent');
+    $definition = $container->getDefinition('jsonapi.serializer');
     $definition->removeMethodCall('setFallbackNormalizer');
 
     $settings = BootstrapConfigStorageFactory::get()
diff --git a/src/Normalizer/ConfigEntityDenormalizer.php b/src/Normalizer/ConfigEntityDenormalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..469bf2f4d302263e972772e581d35c2c6751d874
--- /dev/null
+++ b/src/Normalizer/ConfigEntityDenormalizer.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\jsonapi_extras\Normalizer;
+
+use Drupal\jsonapi\ResourceType\ResourceType;
+
+/**
+ * Override ConfigEntityNormalizer to prepare input.
+ */
+class ConfigEntityDenormalizer extends ContentEntityDenormalizer {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareInput(array $data, ResourceType $resource_type) {
+    foreach ($data as $public_field_name => &$field_value) {
+      /** @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface $enhancer */
+      $enhancer = $resource_type->getFieldEnhancer($public_field_name);
+      if (!$enhancer) {
+        continue;
+      }
+      $field_value = $enhancer->transform($field_value);
+    }
+
+    return $data;
+  }
+
+}
diff --git a/src/Normalizer/ConfigEntityNormalizer.php b/src/Normalizer/ConfigEntityNormalizer.php
deleted file mode 100644
index 003dd06b0434e2da1605187c75846dd06b64c9b7..0000000000000000000000000000000000000000
--- a/src/Normalizer/ConfigEntityNormalizer.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-namespace Drupal\jsonapi_extras\Normalizer;
-
-use Drupal\jsonapi\Normalizer\ConfigEntityNormalizer as JsonapiConfigEntityNormalizer;
-use Drupal\jsonapi\ResourceType\ResourceType;
-
-/**
- * Override ConfigEntityNormalizer to prepare input.
- */
-class ConfigEntityNormalizer extends JsonapiConfigEntityNormalizer {
-
-  use EntityNormalizerTrait;
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getFields($entity, $bundle, ResourceType $resource_type) {
-    $enabled_public_fields = parent::getFields($entity, $bundle, $resource_type);
-    // Then detect if there is any enhancer to be applied here.
-    foreach ($enabled_public_fields as $field_name => &$field_value) {
-      $enhancer = $resource_type->getFieldEnhancer($field_name);
-      if (!$enhancer) {
-        continue;
-      }
-      $field_value = $enhancer->undoTransform($field_value);
-    }
-
-    return $enabled_public_fields;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function prepareInput(array $data, ResourceType $resource_type, $format, array $context) {
-    foreach ($data as $public_field_name => &$field_value) {
-      /** @var \Drupal\jsonapi_extras\Plugin\ResourceFieldEnhancerInterface $enhancer */
-      $enhancer = $resource_type->getFieldEnhancer($public_field_name);
-      if (!$enhancer) {
-        continue;
-      }
-      $field_value = $enhancer->transform($field_value);
-    }
-
-    return parent::prepareInput($data, $resource_type, $format, $context);
-  }
-
-}
diff --git a/src/Normalizer/EntityNormalizerTrait.php b/src/Normalizer/ContentEntityDenormalizer.php
similarity index 67%
rename from src/Normalizer/EntityNormalizerTrait.php
rename to src/Normalizer/ContentEntityDenormalizer.php
index ca16a300a7d96c302323bd55cd9cd61f5d8b4160..b58812ceff45fadc303201180967dfcdbfc203b9 100644
--- a/src/Normalizer/EntityNormalizerTrait.php
+++ b/src/Normalizer/ContentEntityDenormalizer.php
@@ -3,11 +3,41 @@
 namespace Drupal\jsonapi_extras\Normalizer;
 
 use Drupal\jsonapi\ResourceType\ResourceType;
+use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 
 /**
- * Common code for entity normalizers.
+ * Override ContentEntityNormalizer to prepare input.
  */
-trait EntityNormalizerTrait {
+class ContentEntityDenormalizer implements DenormalizerInterface {
+
+  /**
+   * @var \Symfony\Component\Serializer\Normalizer\DenormalizerInterface
+   */
+  protected $inner;
+
+  /**
+   * ContentEntityDenormalizer constructor.
+   *
+   * @param \Symfony\Component\Serializer\Normalizer\DenormalizerInterface $inner
+   *   The JSON:API content entity denormalizer.
+   */
+  public function __construct(DenormalizerInterface $inner) {
+    $this->inner = $inner;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    return $this->inner->denormalize($this->prepareInput($data, $context['resource_type']), $class, $format, $context);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function supportsDenormalization($data, $type, $format = NULL) {
+    return $this->inner->supportsDenormalization($data, $type, $format);
+  }
 
   /**
    * Prepares the input data to create the entity.
@@ -16,15 +46,11 @@ trait EntityNormalizerTrait {
    *   The input data to modify.
    * @param \Drupal\jsonapi\ResourceType\ResourceType $resource_type
    *   Contains the info about the resource type.
-   * @param string $format
-   *   Format from which the given data was extracted.
-   * @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) {
+  protected function prepareInput(array $data, ResourceType $resource_type) {
     /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions */
     $field_storage_definitions = \Drupal::service('entity_field.manager')
       ->getFieldStorageDefinitions(
@@ -37,7 +63,7 @@ trait EntityNormalizerTrait {
       // Skip any disabled field.
       $internal_name = $resource_type->getInternalName($public_field_name);
       $entity_type_id = $resource_type->getEntityTypeId();
-      $entity_type_definition = $this->entityTypeManager->getDefinition($entity_type_id);
+      $entity_type_definition = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
       $uuid_key = $entity_type_definition->getKey('uuid');
       if (!$resource_type->isFieldEnabled($internal_name) && $uuid_key !== $internal_name) {
         continue;
@@ -69,28 +95,7 @@ trait EntityNormalizerTrait {
       $data_internal[$public_field_name] = $field_value;
     }
 
-    return parent::prepareInput($data_internal, $resource_type, $format, $context);
-  }
-
-  /**
-   * Get the configuration entity based on the entity type and bundle.
-   *
-   * @param string $entity_type_id
-   *   The entity type ID.
-   * @param string $bundle_id
-   *   The bundle ID.
-   *
-   * @return \Drupal\Core\Entity\EntityInterface|null
-   *   The resource config entity or NULL.
-   */
-  protected function getResourceConfig($entity_type_id, $bundle_id) {
-    $id = sprintf('%s--%s', $entity_type_id, $bundle_id);
-    // TODO: Inject this service.
-    $resource_config = \Drupal::entityTypeManager()
-      ->getStorage('jsonapi_resource_config')
-      ->load($id);
-
-    return $resource_config;
+    return $data_internal;
   }
 
 }
diff --git a/src/Normalizer/ContentEntityNormalizer.php b/src/Normalizer/ContentEntityNormalizer.php
deleted file mode 100644
index 302866ac5095343a16da4c86caa2ec390656ed49..0000000000000000000000000000000000000000
--- a/src/Normalizer/ContentEntityNormalizer.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-namespace Drupal\jsonapi_extras\Normalizer;
-
-use Drupal\jsonapi\Normalizer\ContentEntityNormalizer as JsonapiContentEntityNormalizer;
-use Symfony\Component\Serializer\SerializerInterface;
-
-/**
- * Override ContentEntityNormalizer to prepare input.
- */
-class ContentEntityNormalizer extends JsonapiContentEntityNormalizer {
-
-  use EntityNormalizerTrait;
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setSerializer(SerializerInterface $serializer) {
-    // The first invocation is made by the container builder, it respects the
-    // service definition. We respect this.
-    // The second invocation is made by the Serializer service constructor, it
-    // does not respect the service definition. We ignore this call.
-    if (!isset($this->serializer)) {
-      parent::setSerializer($serializer);
-    }
-  }
-
-}
diff --git a/src/Normalizer/FieldItemNormalizer.php b/src/Normalizer/FieldItemNormalizer.php
index e993818dc1b43df0c98622b2ec79e501cbd6c764..91982f77c2ebdcca3735ebac63470b69b6d36375 100644
--- a/src/Normalizer/FieldItemNormalizer.php
+++ b/src/Normalizer/FieldItemNormalizer.php
@@ -68,7 +68,7 @@ class FieldItemNormalizer extends NormalizerBase implements DenormalizerInterfac
     $normalized_output = $this->subject->normalize($object, $format, $context);
     // Then detect if there is any enhancer to be applied here.
     /** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type */
-    $resource_type = $context['resource_type'];
+    $resource_type = $context['resource_object']->getResourceType();
     $enhancer = $resource_type->getFieldEnhancer($object->getParent()->getName());
     if (!$enhancer) {
       return $normalized_output;
diff --git a/src/Normalizer/RelationshipItemNormalizer.php b/src/Normalizer/RelationshipItemNormalizer.php
index 2a7d3e7105c5b844119b02fc920e16259f2ce124..dce1417f2b73bc787bc3b9dc0e05684eb5b3dbd7 100644
--- a/src/Normalizer/RelationshipItemNormalizer.php
+++ b/src/Normalizer/RelationshipItemNormalizer.php
@@ -2,15 +2,17 @@
 
 namespace Drupal\jsonapi_extras\Normalizer;
 
+use Drupal\jsonapi\JsonApiResource\ResourceIdentifier;
 use Drupal\jsonapi\Normalizer\NormalizerBase;
 use Drupal\jsonapi\Normalizer\RelationshipItem;
-use Drupal\jsonapi\Normalizer\RelationshipItemNormalizer as RelationshipItemNormalizerJsonapi;
+use Drupal\jsonapi\Normalizer\ResourceIdentifierNormalizer as DecoratedNormalizer;
 use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
 use Drupal\serialization\Normalizer\CacheableNormalizerInterface;
 use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 use Symfony\Component\Serializer\SerializerAwareInterface;
 use Symfony\Component\Serializer\SerializerAwareTrait;
+use Symfony\Component\Serializer\SerializerInterface;
 
 /**
  * Converts the Drupal entity reference item object to a JSON:API structure.
@@ -26,7 +28,7 @@ class RelationshipItemNormalizer extends NormalizerBase implements SerializerAwa
    *
    * @var string
    */
-  protected $supportedInterfaceOrClass = RelationshipItem::class;
+  protected $supportedInterfaceOrClass = ResourceIdentifier::class;
 
   /**
    * The JSON:API base normalizer.
@@ -43,14 +45,14 @@ class RelationshipItemNormalizer extends NormalizerBase implements SerializerAwa
   protected $resourceTypeRepository;
 
   /**
-   * Instantiates a RelationshipItemNormalizer object.
+   * Instantiates a ResourceIdentifierNormalizer object.
    *
-   * @param \Drupal\jsonapi\Normalizer\RelationshipItemNormalizer $subject
+   * @param \Drupal\jsonapi\Normalizer\ResourceIdentifierNormalizer $subject
    *   The decorated normalizer.
    * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
    *   The repository.
    */
-  public function __construct(RelationshipItemNormalizerJsonapi $subject, ResourceTypeRepositoryInterface $resource_type_repository) {
+  public function __construct(DecoratedNormalizer $subject, ResourceTypeRepositoryInterface $resource_type_repository) {
     $this->subject = $subject;
     $this->resourceTypeRepository = $resource_type_repository;
   }
@@ -58,11 +60,20 @@ class RelationshipItemNormalizer extends NormalizerBase implements SerializerAwa
   /**
    * {@inheritdoc}
    */
-  public function normalize($relationship_item, $format = NULL, array $context = []) {
-    $normalized_output = $this->subject->normalize($relationship_item, $format, $context);
+  public function setSerializer(SerializerInterface $serializer) {
+    parent::setSerializer($serializer);
+    $this->subject->setSerializer($serializer);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($field, $format = NULL, array $context = []) {
+    assert($field instanceof ResourceIdentifier);
+    $normalized_output = $this->subject->normalize($field, $format, $context);
     /** @var \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type */
-    $resource_type = $context['resource_type'];
-    $enhancer = $resource_type->getFieldEnhancer($relationship_item->getParent()->getPropertyName());
+    $resource_type = $context['resource_object']->getResourceType();
+    $enhancer = $resource_type->getFieldEnhancer($context['field_name']);
     if (!$enhancer) {
       return $normalized_output;
     }
diff --git a/src/Normalizer/ResourceObjectNormalizer.php b/src/Normalizer/ResourceObjectNormalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..edd463be6465dca123bd4e016d483fbf59e52002
--- /dev/null
+++ b/src/Normalizer/ResourceObjectNormalizer.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\jsonapi_extras\Normalizer;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\jsonapi\JsonApiResource\ResourceObject;
+use Drupal\jsonapi\Normalizer\Value\CacheableNormalization;
+use Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType;
+use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
+
+/**
+ * Decorates the JSON:API ResourceObjectNormalizer.
+ *
+ * @internal
+ */
+class ResourceObjectNormalizer implements NormalizerInterface {
+
+  /**
+   * @var \Symfony\Component\Serializer\Normalizer\NormalizerInterface
+   */
+  protected $inner;
+
+  public function __construct(NormalizerInterface $inner) {
+    $this->inner = $inner;
+  }
+
+  public function supportsNormalization($data, $format = NULL) {
+    return $this->inner->supportsNormalization($data, $format);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = []) {
+    assert($object instanceof ResourceObject);
+    $resource_type = $object->getResourceType();
+    $cacheable_normalization = $this->inner->normalize($object, $format, $context);
+    assert($cacheable_normalization instanceof CacheableNormalization);
+    if (is_subclass_of($resource_type->getDeserializationTargetClass(), ConfigEntityInterface::class)) {
+      return new CacheableNormalization(
+        $cacheable_normalization,
+        static::enhanceConfigFields($cacheable_normalization->getNormalization(), $resource_type)
+      );
+    }
+    return $cacheable_normalization;
+  }
+
+  /**
+   * Applies field enhancers to a config entity normalization.
+   *
+   * @param array $normalization
+   *   The normalization to be enhanced.
+   * @param \Drupal\jsonapi_extras\ResourceType\ConfigurableResourceType $resource_type
+   *   The resource type of the normalized resource object.
+   *
+   * @return array
+   */
+  protected static function enhanceConfigFields(array $normalization, ConfigurableResourceType $resource_type) {
+    if (!empty($normalization['attributes'])) {
+      foreach ($normalization['attributes'] as $field_name => $field_value) {
+        $enhancer = $resource_type->getFieldEnhancer($field_name);
+        if (!$enhancer) {
+          continue;
+        }
+        $normalization[$field_name] = $enhancer->undoTransform($field_value);
+      }
+    }
+    return $normalization;
+  }
+
+}
\ No newline at end of file
diff --git a/tests/src/Kernel/Controller/EntityResourceTest.php b/tests/src/Kernel/Controller/EntityResourceTest.php
index c55515552548b1978c55b9260a1a91eee9907600..1e5e077a47704bed3d75e6a6ac0fdcbf18c1e91d 100644
--- a/tests/src/Kernel/Controller/EntityResourceTest.php
+++ b/tests/src/Kernel/Controller/EntityResourceTest.php
@@ -5,6 +5,7 @@ namespace Drupal\Tests\jsonapi_extras\Kernel\Controller;
 use Drupal\Component\Serialization\Json;
 use Drupal\Core\Config\ConfigException;
 use Drupal\jsonapi\Access\EntityAccessChecker;
+use Drupal\jsonapi\JsonApiResource\ResourceObject;
 use Drupal\jsonapi\ResourceType\ResourceType;
 use Drupal\jsonapi\Controller\EntityResource;
 use Drupal\jsonapi\JsonApiResource\JsonApiDocumentTopLevel as JsonApiDocumentTopLevel2;
@@ -16,7 +17,7 @@ use Symfony\Component\HttpFoundation\Request;
 
 /**
  * @coversDefaultClass \Drupal\jsonapi\Controller\EntityResource
- * @covers \Drupal\jsonapi_extras\Normalizer\ConfigEntityNormalizer
+ * @covers \Drupal\jsonapi_extras\Normalizer\ConfigEntityDenormalizer
  * @group jsonapi_extras
  * @group legacy
  *
@@ -53,15 +54,21 @@ class EntityResourceTest extends KernelTestBase {
    * @covers ::createIndividual
    */
   public function testCreateIndividualConfig() {
-    $node_type = NodeType::create([
-      'type' => 'test',
-      'name' => 'Test Type',
-      'description' => 'Lorem ipsum',
-    ]);
     Role::load(Role::ANONYMOUS_ID)
       ->grantPermission('administer content types')
       ->save();
-    $resource_type = new ResourceType('node', 'article', NULL);
+    $resource_type = new ResourceType('node_type', 'node_type', NodeType::class);
+    $resource_type->setRelatableResourceTypes([]);
+    $payload = Json::encode([
+      'data' => [
+        'type' => 'node--test',
+        'attributes' => [
+          'type' => 'test',
+          'name' => 'Test Type',
+          'description' => 'Lorem ipsum',
+        ],
+      ],
+    ]);
     $entity_resource = new EntityResource(
       $this->container->get('entity_type.manager'),
       $this->container->get('entity_field.manager'),
@@ -75,13 +82,17 @@ class EntityResourceTest extends KernelTestBase {
         $this->container->get('router.no_access_checks'),
         $this->container->get('current_user'),
         $this->container->get('entity.repository')
-      )
+      ),
+      $this->container->get('jsonapi.field_resolver'),
+      $this->container->get('jsonapi.serializer')
     );
-    $response = $entity_resource->createIndividual($resource_type, $node_type, Request::create('/jsonapi/node/test'));
+    $response = $entity_resource->createIndividual($resource_type, Request::create('/jsonapi/node_type/node_type', 'POST', [], [], [], [], $payload));
     // As a side effect, the node type will also be saved.
-    $this->assertNotEmpty($node_type->id());
+    $node_type = NodeType::load('test');
     $this->assertInstanceOf(JsonApiDocumentTopLevel2::class, $response->getResponseData());
-    $this->assertEquals('test', $response->getResponseData()->getData()->id());
+    $data = $response->getResponseData()->getData();
+    $this->assertInstanceOf(ResourceObject::class, $data);
+    $this->assertEquals($node_type->uuid(), $data->getId());
     $this->assertEquals(201, $response->getStatusCode());
   }
 
@@ -108,14 +119,17 @@ class EntityResourceTest extends KernelTestBase {
       ->save();
     $payload = Json::encode([
       'data' => [
-        'type' => 'node_type',
+        'type' => 'node_type--node_type',
         'id' => $node_type->uuid(),
         'attributes' => $values,
       ],
     ]);
     $request = Request::create('/jsonapi/node/node_type/' . $node_type->uuid(), 'PATCH', [], [], [], [], $payload);
 
-    $resource_type = new ResourceType('node', 'article', NULL);
+    $resource_type = new ResourceType('node_type', 'node_type', NodeType::class, FALSE, TRUE, TRUE, FALSE, [
+      'type' => 'drupal_internal__type',
+    ]);
+    $resource_type->setRelatableResourceTypes([]);
     $entity_resource = new EntityResource(
       $this->container->get('entity_type.manager'),
       $this->container->get('entity_field.manager'),
@@ -129,14 +143,16 @@ class EntityResourceTest extends KernelTestBase {
         $this->container->get('router.no_access_checks'),
         $this->container->get('current_user'),
         $this->container->get('entity.repository')
-      )
+      ),
+      $this->container->get('jsonapi.field_resolver'),
+      $this->container->get('jsonapi.serializer')
     );
-    $response = $entity_resource->patchIndividual($resource_type, $node_type, $parsed_node_type, $request);
+    $response = $entity_resource->patchIndividual($resource_type, $node_type, $request);
 
     // As a side effect, the node will also be saved.
     $this->assertInstanceOf(JsonApiDocumentTopLevel2::class, $response->getResponseData());
     $updated_node_type = $response->getResponseData()->getData();
-    $this->assertInstanceOf(NodeType::class, $updated_node_type);
+    $this->assertInstanceOf(ResourceObject::class, $updated_node_type);
     // If the field is ignored then we should not see a difference.
     foreach ($values as $field_name => $value) {
       in_array($field_name, $ignored_fields) ?
@@ -163,8 +179,8 @@ class EntityResourceTest extends KernelTestBase {
    * @covers ::patchIndividual
    * @dataProvider patchIndividualConfigFailedProvider
    */
-  public function testPatchIndividualFailedConfig($values) {
-    $this->setExpectedException(ConfigException::class);
+  public function testPatchIndividualFailedConfig($values, $expected_message) {
+    $this->setExpectedException(ConfigException::class, $expected_message);
     $this->testPatchIndividualConfig($values);
   }
 
@@ -176,8 +192,7 @@ class EntityResourceTest extends KernelTestBase {
    */
   public function patchIndividualConfigFailedProvider() {
     return [
-      [['uuid' => 'PATCHED']],
-      [['type' => 'article', 'status' => FALSE]],
+      [['type' => 'article', 'status' => FALSE], "The machine name of the 'Content type' bundle cannot be changed."],
     ];
   }
 
@@ -208,7 +223,9 @@ class EntityResourceTest extends KernelTestBase {
         $this->container->get('router.no_access_checks'),
         $this->container->get('current_user'),
         $this->container->get('entity.repository')
-      )
+      ),
+      $this->container->get('jsonapi.field_resolver'),
+      $this->container->get('jsonapi.serializer')
     );
     $response = $entity_resource->deleteIndividual($node_type, new Request());
     // As a side effect, the node will also be deleted.