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

Issue:
SerializerMethodResourceRelatedField breaks parent chain
If `SerializerMethodResourceRelatedField` is called with `many=True` and creates child field it misses call to `bind()` which would complete setting the relation between parent field and child field.

In effect it breaks the pattern that on every DRF field you can always go up the tree towards the root using `parent` attribute. 

That part of [ManyRelatedField.__init__()](https://github.com/encode/django-rest-framework/blob/master/rest_framework/relations.py#L484) is missing:

```
class ManyRelatedField:
    ...
    def __init__(self, child_relation=None, *args, **kwargs):
        self.child_relation = child_relation
        ...
        self.child_relation.bind(field_name='', parent=self)
```

I am aware that there are some issues related to `SerializerMethodResourceRelatedField` already reported in #639, but since this bug is completely independent thing, I report it as separate issue.


Discussion:
Thanks for the hinter. I agree this is not directly related to #639 and would be an easier fix.

PR is also welcome for whoever want to pick this up.

Resolving patch:
diff --git a/example/serializers.py b/example/serializers.py
--- a/example/serializers.py
+++ b/example/serializers.py
@@ -126,30 +126,24 @@ def __init__(self, *args, **kwargs):
         related_link_view_name='entry-suggested',
         related_link_url_kwarg='entry_pk',
         self_link_view_name='entry-relationships',
-        source='get_suggested',
         model=Entry,
         many=True,
-        read_only=True
     )
     # many related hyperlinked from serializer
     suggested_hyperlinked = relations.SerializerMethodHyperlinkedRelatedField(
         related_link_view_name='entry-suggested',
         related_link_url_kwarg='entry_pk',
         self_link_view_name='entry-relationships',
-        source='get_suggested',
         model=Entry,
         many=True,
-        read_only=True
     )
     # single related from serializer
-    featured = relations.SerializerMethodResourceRelatedField(
-        source='get_featured', model=Entry, read_only=True)
+    featured = relations.SerializerMethodResourceRelatedField(model=Entry)
     # single related hyperlinked from serializer
     featured_hyperlinked = relations.SerializerMethodHyperlinkedRelatedField(
         related_link_view_name='entry-featured',
         related_link_url_kwarg='entry_pk',
         self_link_view_name='entry-relationships',
-        source='get_featured',
         model=Entry,
         read_only=True
     )
@@ -229,8 +223,6 @@ class AuthorSerializer(serializers.ModelSerializer):
         related_link_view_name='author-related',
         self_link_view_name='author-relationships',
         model=Entry,
-        read_only=True,
-        source='get_first_entry'
     )
     comments = relations.HyperlinkedRelatedField(
         related_link_view_name='author-related',
diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py
--- a/rest_framework_json_api/relations.py
+++ b/rest_framework_json_api/relations.py
@@ -1,12 +1,12 @@
 import json
+import warnings
 from collections import OrderedDict
-from collections.abc import Iterable
 
 import inflection
 from django.core.exceptions import ImproperlyConfigured
 from django.urls import NoReverseMatch
 from django.utils.translation import gettext_lazy as _
-from rest_framework.fields import MISSING_ERROR_MESSAGE, SkipField
+from rest_framework.fields import MISSING_ERROR_MESSAGE, Field, SkipField
 from rest_framework.relations import MANY_RELATION_KWARGS
 from rest_framework.relations import ManyRelatedField as DRFManyRelatedField
 from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
@@ -347,51 +347,63 @@ def to_internal_value(self, data):
         return super(ResourceRelatedField, self).to_internal_value(data['id'])
 
 
-class SerializerMethodResourceRelatedField(ResourceRelatedField):
+class SerializerMethodFieldBase(Field):
+    def __init__(self, method_name=None, **kwargs):
+        if not method_name and kwargs.get('source'):
+            method_name = kwargs.pop('source')
+            warnings.warn(DeprecationWarning(
+                "'source' argument of {cls} is deprecated, use 'method_name' "
+                "as in SerializerMethodField".format(cls=self.__class__.__name__)), stacklevel=3)
+        self.method_name = method_name
+        kwargs['source'] = '*'
+        kwargs['read_only'] = True
+        super().__init__(**kwargs)
+
+    def bind(self, field_name, parent):
+        default_method_name = 'get_{field_name}'.format(field_name=field_name)
+        if self.method_name is None:
+            self.method_name = default_method_name
+        super().bind(field_name, parent)
+
+    def get_attribute(self, instance):
+        serializer_method = getattr(self.parent, self.method_name)
+        return serializer_method(instance)
+
+
+class ManySerializerMethodResourceRelatedField(SerializerMethodFieldBase, ResourceRelatedField):
+    def __init__(self, child_relation=None, *args, **kwargs):
+        assert child_relation is not None, '`child_relation` is a required argument.'
+        self.child_relation = child_relation
+        super().__init__(**kwargs)
+        self.child_relation.bind(field_name='', parent=self)
+
+    def to_representation(self, value):
+        return [self.child_relation.to_representation(item) for item in value]
+
+
+class SerializerMethodResourceRelatedField(SerializerMethodFieldBase, ResourceRelatedField):
     """
     Allows us to use serializer method RelatedFields
     with return querysets
     """
-    def __new__(cls, *args, **kwargs):
-        """
-        We override this because getting serializer methods
-        fails at the base class when many=True
-        """
-        if kwargs.pop('many', False):
-            return cls.many_init(*args, **kwargs)
-        return super(ResourceRelatedField, cls).__new__(cls, *args, **kwargs)
 
-    def __init__(self, child_relation=None, *args, **kwargs):
-        model = kwargs.pop('model', None)
-        if child_relation is not None:
-            self.child_relation = child_relation
-        if model:
-            self.model = model
-        super(SerializerMethodResourceRelatedField, self).__init__(*args, **kwargs)
+    many_kwargs = [*MANY_RELATION_KWARGS, *LINKS_PARAMS, 'method_name', 'model']
+    many_cls = ManySerializerMethodResourceRelatedField
 
     @classmethod
     def many_init(cls, *args, **kwargs):
-        list_kwargs = {k: kwargs.pop(k) for k in LINKS_PARAMS if k in kwargs}
-        list_kwargs['child_relation'] = cls(*args, **kwargs)
-        for key in kwargs.keys():
-            if key in ('model',) + MANY_RELATION_KWARGS:
+        list_kwargs = {'child_relation': cls(**kwargs)}
+        for key in kwargs:
+            if key in cls.many_kwargs:
                 list_kwargs[key] = kwargs[key]
-        return cls(**list_kwargs)
+        return cls.many_cls(**list_kwargs)
 
-    def get_attribute(self, instance):
-        # check for a source fn defined on the serializer instead of the model
-        if self.source and hasattr(self.parent, self.source):
-            serializer_method = getattr(self.parent, self.source)
-            if hasattr(serializer_method, '__call__'):
-                return serializer_method(instance)
-        return super(SerializerMethodResourceRelatedField, self).get_attribute(instance)
 
-    def to_representation(self, value):
-        if isinstance(value, Iterable):
-            base = super(SerializerMethodResourceRelatedField, self)
-            return [base.to_representation(x) for x in value]
-        return super(SerializerMethodResourceRelatedField, self).to_representation(value)
+class ManySerializerMethodHyperlinkedRelatedField(SkipDataMixin,
+                                                  ManySerializerMethodResourceRelatedField):
+    pass
 
 
-class SerializerMethodHyperlinkedRelatedField(SkipDataMixin, SerializerMethodResourceRelatedField):
-    pass
+class SerializerMethodHyperlinkedRelatedField(SkipDataMixin,
+                                              SerializerMethodResourceRelatedField):
+    many_cls = ManySerializerMethodHyperlinkedRelatedField


Test patch:
diff --git a/example/tests/test_parsers.py b/example/tests/test_parsers.py
--- a/example/tests/test_parsers.py
+++ b/example/tests/test_parsers.py
@@ -4,7 +4,7 @@
 from django.conf.urls import url
 from django.test import TestCase, override_settings
 from django.urls import reverse
-from rest_framework import views, status
+from rest_framework import status, views
 from rest_framework.exceptions import ParseError
 from rest_framework.response import Response
 from rest_framework.test import APITestCase
diff --git a/example/tests/test_relations.py b/example/tests/test_relations.py
--- a/example/tests/test_relations.py
+++ b/example/tests/test_relations.py
@@ -290,16 +290,12 @@ class EntryModelSerializerWithHyperLinks(serializers.ModelSerializer):
         related_link_url_kwarg='entry_pk',
         self_link_view_name='entry-relationships',
         many=True,
-        read_only=True,
-        source='get_blog'
     )
     comments = SerializerMethodHyperlinkedRelatedField(
         related_link_view_name='entry-comments',
         related_link_url_kwarg='entry_pk',
         self_link_view_name='entry-relationships',
         many=True,
-        read_only=True,
-        source='get_comments'
     )
 
     class Meta:
diff --git a/example/tests/test_serializers.py b/example/tests/test_serializers.py
--- a/example/tests/test_serializers.py
+++ b/example/tests/test_serializers.py
@@ -7,21 +7,17 @@
 from rest_framework.request import Request
 from rest_framework.test import APIRequestFactory
 
-from example.factories import ArtProjectFactory
 from rest_framework_json_api.serializers import (
     DateField,
     ModelSerializer,
     ResourceIdentifierObjectSerializer,
-    empty,
+    empty
 )
 from rest_framework_json_api.utils import format_resource_type
 
+from example.factories import ArtProjectFactory
 from example.models import Author, Blog, Entry
-from example.serializers import (
-    BlogSerializer,
-    ProjectSerializer,
-    ArtProjectSerializer,
-)
+from example.serializers import ArtProjectSerializer, BlogSerializer, ProjectSerializer
 
 request_factory = APIRequestFactory()
 pytestmark = pytest.mark.django_db
diff --git a/example/tests/unit/test_serializer_method_field.py b/example/tests/unit/test_serializer_method_field.py
new file mode 100644
--- /dev/null
+++ b/example/tests/unit/test_serializer_method_field.py
@@ -0,0 +1,66 @@
+from __future__ import absolute_import
+
+import pytest
+from rest_framework import serializers
+
+from rest_framework_json_api.relations import SerializerMethodResourceRelatedField
+
+from example.models import Blog, Entry
+
+
+def test_method_name_default():
+    class BlogSerializer(serializers.ModelSerializer):
+        one_entry = SerializerMethodResourceRelatedField(model=Entry)
+
+        class Meta:
+            model = Blog
+            fields = ['one_entry']
+
+        def get_one_entry(self, instance):
+            return Entry(id=100)
+
+    serializer = BlogSerializer(instance=Blog())
+    assert serializer.data['one_entry']['id'] == '100'
+
+
+def test_method_name_custom():
+    class BlogSerializer(serializers.ModelSerializer):
+        one_entry = SerializerMethodResourceRelatedField(
+            model=Entry,
+            method_name='get_custom_entry'
+        )
+
+        class Meta:
+            model = Blog
+            fields = ['one_entry']
+
+        def get_custom_entry(self, instance):
+            return Entry(id=100)
+
+    serializer = BlogSerializer(instance=Blog())
+    assert serializer.data['one_entry']['id'] == '100'
+
+
+@pytest.mark.filterwarnings("ignore::DeprecationWarning")
+def test_source():
+    class BlogSerializer(serializers.ModelSerializer):
+        one_entry = SerializerMethodResourceRelatedField(
+            model=Entry,
+            source='get_custom_entry'
+        )
+
+        class Meta:
+            model = Blog
+            fields = ['one_entry']
+
+        def get_custom_entry(self, instance):
+            return Entry(id=100)
+
+    serializer = BlogSerializer(instance=Blog())
+    assert serializer.data['one_entry']['id'] == '100'
+
+
+@pytest.mark.filterwarnings("error::DeprecationWarning")
+def test_source_is_deprecated():
+    with pytest.raises(DeprecationWarning):
+        SerializerMethodResourceRelatedField(model=Entry, source='get_custom_entry')