Repository: django-json-api/django-rest-framework-json-api

Issue:
included_serializers should be imported in meta class
There is a method `utils.get_included_serializers` which handles importing of included serializers as a serializer can also be defined as string.

This method is called a couple of times during one request (e.g. in the renderer, resource validation etc.). It is much cleaner though to import this in the serializer meta class at import time so it only needs to be done once. In the code someone can then easily access serializer.included_serializers without using a utility.

This should also bring a performance benefit (even though this would be very small I guess).

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/926?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=django-json-api) Report
> :exclamation: No coverage uploaded for pull request base (`master@5e2f60d`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=django-json-api#section-missing-base-commit).
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/926/graphs/tree.svg?width=650&height=150&src=pr&token=kJ4T4hcVh8&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=django-json-api)](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/926?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=django-json-api)

```diff
@@            Coverage Diff            @@
##             master     #926   +/-   ##
=========================================
  Coverage          ?   97.64%           
=========================================
  Files             ?       58           
  Lines             ?     3061           
  Branches          ?        0           
=========================================
  Hits              ?     2989           
  Misses            ?       72           
  Partials          ?        0           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/926?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=django-json-api).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=django-json-api)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/926?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=django-json-api). Last update [5e2f60d...f0e7560](https://codecov.io/gh/django-json-api/django-rest-framework-json-api/pull/926?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=django-json-api). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=django-json-api).

SafaAlfulaij: Changes done.
But I'm not sure what to do with "self" and inline self-related serializers.

SafaAlfulaij: Ping?
SafaAlfulaij: I'm sorry but I can't work on this anymore. I tried an approach where I set `included_serializers` and `related_serializers` to `None` if not provided, but it resulted in a bit more complicated code, you'll find it before applying the commit "Simplify caching included serializers".

sliverc: @SafaAlfulaij Thanks for having worked on this. I made the last minor changes and this PR should be ready now and be merged once CI is green.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3206cfa..d9eaf50 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 Note that in line with [Django REST Framework policy](http://www.django-rest-framework.org/topics/release-notes/),
 any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change.
 
+## [Unreleased]
+
+### Changed
+
+* Moved resolving of `included_serialzers` and `related_serializers` classes to serializer's meta class.
+
+### Deprecated
+
+* Deprecated `get_included_serializers(serializer)` function under `rest_framework_json_api.utils`. Use `serializer.included_serializers` instead.
+
 ## [4.2.1] - 2021-07-06
 
 ### Fixed
@@ -40,7 +50,6 @@ any parts of the framework not mentioned in the documentation should generally b
 * Deprecated default `format_type` argument of `rest_framework_json_api.utils.format_value`. Use `rest_framework_json_api.utils.format_field_name` or specify specifc `format_type` instead.
 * Deprecated `format_type` argument of `rest_framework_json_api.utils.format_link_segment`. Use `rest_framework_json_api.utils.format_value` instead.
 
-
 ## [4.1.0] - 2021-03-08
 
 ### Added
diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py
index c9dd765..605f7c1 100644
--- a/rest_framework_json_api/relations.py
+++ b/rest_framework_json_api/relations.py
@@ -16,7 +16,6 @@ from rest_framework_json_api.exceptions import Conflict
 from rest_framework_json_api.utils import (
     Hyperlink,
     format_link_segment,
-    get_included_serializers,
     get_resource_type_from_instance,
     get_resource_type_from_queryset,
     get_resource_type_from_serializer,
@@ -274,7 +273,7 @@ class ResourceRelatedField(HyperlinkedMixin, PrimaryKeyRelatedField):
                 inflection.singularize(field_name),
                 inflection.pluralize(field_name),
             ]
-            includes = get_included_serializers(parent)
+            includes = getattr(parent, "included_serializers", dict())
             for field in field_names:
                 if field in includes.keys():
                     return get_resource_type_from_serializer(includes[field])
diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py
index b50c2b1..3c64933 100644
--- a/rest_framework_json_api/renderers.py
+++ b/rest_framework_json_api/renderers.py
@@ -284,7 +284,9 @@ class JSONRenderer(renderers.JSONRenderer):
 
         current_serializer = fields.serializer
         context = current_serializer.context
-        included_serializers = utils.get_included_serializers(current_serializer)
+        included_serializers = getattr(
+            current_serializer, "included_serializers", dict()
+        )
         included_resources = copy.copy(included_resources)
         included_resources = [
             inflection.underscore(value) for value in included_resources
@@ -692,8 +694,8 @@ class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
         included_serializers = []
         already_seen.add(serializer)
 
-        for include, included_serializer in utils.get_included_serializers(
-            serializer
+        for include, included_serializer in getattr(
+            serializer, "included_serializers", dict()
         ).items():
             included_serializers.append(f"{prefix}{include}")
             included_serializers.extend(
diff --git a/rest_framework_json_api/schemas/openapi.py b/rest_framework_json_api/schemas/openapi.py
index 77a0d4e..efe1491 100644
--- a/rest_framework_json_api/schemas/openapi.py
+++ b/rest_framework_json_api/schemas/openapi.py
@@ -1,7 +1,6 @@
 import warnings
 from urllib.parse import urljoin
 
-from django.utils.module_loading import import_string as import_class_from_dotted_path
 from rest_framework.fields import empty
 from rest_framework.relations import ManyRelatedField
 from rest_framework.schemas import openapi as drf_openapi
@@ -379,13 +378,7 @@ class SchemaGenerator(drf_openapi.SchemaGenerator):
         """
         for path, method, view in view_endpoints:
             view_serializer = view.get_serializer()
-            if not isinstance(related_serializer, type):
-                related_serializer_class = import_class_from_dotted_path(
-                    related_serializer
-                )
-            else:
-                related_serializer_class = related_serializer
-            if isinstance(view_serializer, related_serializer_class):
+            if isinstance(view_serializer, related_serializer):
                 return view
 
         return None
diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py
index a73a5d4..bc60f19 100644
--- a/rest_framework_json_api/serializers.py
+++ b/rest_framework_json_api/serializers.py
@@ -1,8 +1,10 @@
 from collections import OrderedDict
+from collections.abc import Mapping
 
 import inflection
 from django.core.exceptions import ObjectDoesNotExist
 from django.db.models.query import QuerySet
+from django.utils.module_loading import import_string as import_class_from_dotted_path
 from django.utils.translation import gettext_lazy as _
 from rest_framework.exceptions import ParseError
 
@@ -22,7 +24,6 @@ from rest_framework_json_api.exceptions import Conflict
 from rest_framework_json_api.relations import ResourceRelatedField
 from rest_framework_json_api.utils import (
     get_included_resources,
-    get_included_serializers,
     get_resource_type_from_instance,
     get_resource_type_from_model,
     get_resource_type_from_serializer,
@@ -120,7 +121,7 @@ class IncludedResourcesValidationMixin(object):
         view = context.get("view") if context else None
 
         def validate_path(serializer_class, field_path, path):
-            serializers = get_included_serializers(serializer_class)
+            serializers = getattr(serializer_class, "included_serializers", None)
             if serializers is None:
                 raise ParseError("This endpoint does not support the include parameter")
             this_field_name = inflection.underscore(field_path[0])
@@ -152,8 +153,55 @@ class IncludedResourcesValidationMixin(object):
         super(IncludedResourcesValidationMixin, self).__init__(*args, **kwargs)
 
 
+class LazySerializersDict(Mapping):
+    """
+    A dictionary of serializers which lazily import dotted class path and self.
+    """
+
+    def __init__(self, parent, serializers):
+        self.parent = parent
+        self.serializers = serializers
+
+    def __getitem__(self, key):
+        value = self.serializers[key]
+        if not isinstance(value, type):
+            if value == "self":
+                value = self.parent
+            else:
+                value = import_class_from_dotted_path(value)
+            self.serializers[key] = value
+
+        return value
+
+    def __iter__(self):
+        return iter(self.serializers)
+
+    def __len__(self):
+        return len(self.serializers)
+
+    def __repr__(self):
+        return dict.__repr__(self.serializers)
+
+
 class SerializerMetaclass(SerializerMetaclass):
-    pass
+    def __new__(cls, name, bases, attrs):
+        serializer = super().__new__(cls, name, bases, attrs)
+
+        if attrs.get("included_serializers", None):
+            setattr(
+                serializer,
+                "included_serializers",
+                LazySerializersDict(serializer, attrs["included_serializers"]),
+            )
+
+        if attrs.get("related_serializers", None):
+            setattr(
+                serializer,
+                "related_serializers",
+                LazySerializersDict(serializer, attrs["related_serializers"]),
+            )
+
+        return serializer
 
 
 # If user imports serializer from here we can catch class definition and check
diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py
index ac31979..19c7280 100644
--- a/rest_framework_json_api/utils.py
+++ b/rest_framework_json_api/utils.py
@@ -1,4 +1,3 @@
-import copy
 import inspect
 import operator
 import warnings
@@ -13,7 +12,6 @@ from django.db.models.fields.related_descriptors import (
 )
 from django.http import Http404
 from django.utils import encoding
-from django.utils.module_loading import import_string as import_class_from_dotted_path
 from django.utils.translation import gettext_lazy as _
 from rest_framework import exceptions
 from rest_framework.exceptions import APIException
@@ -342,20 +340,14 @@ def get_default_included_resources_from_serializer(serializer):
 
 
 def get_included_serializers(serializer):
-    included_serializers = copy.copy(
-        getattr(serializer, "included_serializers", dict())
+    warnings.warn(
+        DeprecationWarning(
+            "Using of `get_included_serializers(serializer)` function is deprecated."
+            "Use `serializer.included_serializers` instead."
+        )
     )
 
-    for name, value in iter(included_serializers.items()):
-        if not isinstance(value, type):
-            if value == "self":
-                included_serializers[name] = (
-                    serializer if isinstance(serializer, type) else serializer.__class__
-                )
-            else:
-                included_serializers[name] = import_class_from_dotted_path(value)
-
-    return included_serializers
+    return getattr(serializer, "included_serializers", dict())
 
 
 def get_relation_instance(resource_instance, source, serializer):
diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py
index bb6f09b..6b73958 100644
--- a/rest_framework_json_api/views.py
+++ b/rest_framework_json_api/views.py
@@ -11,7 +11,6 @@ from django.db.models.fields.related_descriptors import (
 from django.db.models.manager import Manager
 from django.db.models.query import QuerySet
 from django.urls import NoReverseMatch
-from django.utils.module_loading import import_string as import_class_from_dotted_path
 from rest_framework import generics, viewsets
 from rest_framework.exceptions import MethodNotAllowed, NotFound
 from rest_framework.fields import get_attribute
@@ -183,8 +182,6 @@ class RelatedMixin(object):
                     False
                 ), 'Either "included_serializers" or "related_serializers" should be configured'
 
-            if not isinstance(_class, type):
-                return import_class_from_dotted_path(_class)
             return _class
 
         return parent_serializer_class


Test patch:
diff --git a/tests/test_serializers.py b/tests/test_serializers.py
new file mode 100644
index 0000000..6726fa8
--- /dev/null
+++ b/tests/test_serializers.py
@@ -0,0 +1,35 @@
+from django.db import models
+
+from rest_framework_json_api import serializers
+from tests.models import DJAModel, ManyToManyTarget
+from tests.serializers import ManyToManyTargetSerializer
+
+
+def test_get_included_serializers():
+    class IncludedSerializersModel(DJAModel):
+        self = models.ForeignKey("self", on_delete=models.CASCADE)
+        target = models.ForeignKey(ManyToManyTarget, on_delete=models.CASCADE)
+        other_target = models.ForeignKey(ManyToManyTarget, on_delete=models.CASCADE)
+
+        class Meta:
+            app_label = "tests"
+
+    class IncludedSerializersSerializer(serializers.ModelSerializer):
+        included_serializers = {
+            "self": "self",
+            "target": ManyToManyTargetSerializer,
+            "other_target": "tests.serializers.ManyToManyTargetSerializer",
+        }
+
+        class Meta:
+            model = IncludedSerializersModel
+            fields = ("self", "other_target", "target")
+
+    included_serializers = IncludedSerializersSerializer.included_serializers
+    expected_included_serializers = {
+        "self": IncludedSerializersSerializer,
+        "target": ManyToManyTargetSerializer,
+        "other_target": ManyToManyTargetSerializer,
+    }
+
+    assert included_serializers == expected_included_serializers
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 00bf083..43c12dc 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -346,7 +346,7 @@ def test_get_related_resource_type_from_plain_serializer_class(
 
 
 def test_get_included_serializers():
-    class IncludedSerializersModel(DJAModel):
+    class DeprecatedIncludedSerializersModel(DJAModel):
         self = models.ForeignKey("self", on_delete=models.CASCADE)
         target = models.ForeignKey(ManyToManyTarget, on_delete=models.CASCADE)
         other_target = models.ForeignKey(ManyToManyTarget, on_delete=models.CASCADE)
@@ -354,7 +354,7 @@ def test_get_included_serializers():
         class Meta:
             app_label = "tests"
 
-    class IncludedSerializersSerializer(serializers.ModelSerializer):
+    class DeprecatedIncludedSerializersSerializer(serializers.ModelSerializer):
         included_serializers = {
             "self": "self",
             "target": ManyToManyTargetSerializer,
@@ -362,12 +362,16 @@ def test_get_included_serializers():
         }
 
         class Meta:
-            model = IncludedSerializersModel
+            model = DeprecatedIncludedSerializersModel
             fields = ("self", "other_target", "target")
 
-    included_serializers = get_included_serializers(IncludedSerializersSerializer)
+    with pytest.deprecated_call():
+        included_serializers = get_included_serializers(
+            DeprecatedIncludedSerializersSerializer
+        )
+
     expected_included_serializers = {
-        "self": IncludedSerializersSerializer,
+        "self": DeprecatedIncludedSerializersSerializer,
         "target": ManyToManyTargetSerializer,
         "other_target": ManyToManyTargetSerializer,
     }