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

Issue:
Reverse the relationship names in includes
I am not a 100% sure about this, but when all resource names are dasherized and pluralized, shouldn't  the included names be plural and dasherized too?

By included names I mean:
http://jsonapi.org/format/#fetching-includes

Right now, I retrieve data like this: `/api/v1/vehicle-checks?include=calendar_block`, which shows the mix in naming style: the ressource is dasherized and pluralized, the included ressource is not. 

This request works, and ``?include=calendar-blocks` does not, which is obvious when looking at serializers.py lines 75 - 80.

So my question is, shouldn't some reversing occur to match the naming style, similar to how the output is changed?

inflection is not performed by AutoPrefetchMixin for included resources
On v4.0.0, inflection is not performed by the AutoPrefetchMixin for included resources, contrary to the JSONRenderer. 

Suppose the following data model, serializers and viewsets:

```
from django.db import models
from rest_framework import viewsets, mixins
from rest_framework_json_api import views, serializers

class Album(models.Model):
    title = models.TextField()

class Track(models.Model)
    title = models.TextField()
    is_part_of = models.ManyToManyField(Album)

class AlbumSerializer(serializers.ModelSerializer):
    class Meta:
        model = Album
        fields = ["title"]

class TrackSerializer(serializers.ModelSerializer):
    class Meta:
        model = Track
        fields = ["title", "is_part_of"]

    included_serializers = {
        "is_part_of": AlbumSerializer,
    }
```

```
class Track(
    views.AutoPrefetchMixin,
    views.RelatedMixin,
    mixins.ListModelMixin,
    viewsets.GenericViewSet
):
    serializer_class = serializers.TrackSerializer
    queryset = Track.objects.all()
```

Both `GET /tracks?include=is-part-of` and `GET /tracks?include=isPartOf` correctly return the set all of tracks along with their albums' titles embedded within the `included` section of the response.

However, prefetch of the tracks' related album objects is not performed, as one would expect from `views.AutoPrefetchMixin`.

My workaround is to perform `GET tracks?include=is_part_of`, which returns the same thing as the above two queries but also performs prefetching.

My investigation has shown me that the `AutoPrefetchMixin` does not perform inflection of the result of `get_included_resources` that it uses to create the argument passed to `prefetch_related`. The JSONRenderer does the following instead:

```
        included_resources = copy.copy(included_resources)
        included_resources = [inflection.underscore(value) for value in included_resources]
```


SparseFieldsetsMixin does not use the correct configured JSON_API_FORMAT_FIELD_NAMES
## Description of the Bug Report
`SparseFieldsetsMixin` does not adjust field_names by configured format.

## Fix:


```python
class SparseFieldsetsMixin:
    """
    A serializer mixin that adds support for sparse fieldsets through `fields` query parameter.

    Specification: https://jsonapi.org/format/#fetching-sparse-fieldsets
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        context = kwargs.get("context")
        request = context.get("request") if context else None

        if request:
            sparse_fieldset_query_param = "fields[{}]".format(
                get_resource_type_from_serializer(self)
            )
            try:
                param_name = next(
                    key
                    for key in request.query_params
                    if sparse_fieldset_query_param == key
                )
            except StopIteration:
                pass
            else:
                fieldset = request.query_params.get(param_name).split(",")
                # iterate over a *copy* of self.fields' underlying OrderedDict, because we may
                # modify the original during the iteration.
                # self.fields is a `rest_framework.utils.serializer_helpers.BindingDict`
                for field_name, field in self.fields.fields.copy().items():
                    if (
                        field_name == api_settings.URL_FIELD_NAME
                    ):  # leave self link there
                        continue
                    # missing format_value()
                    correct_field_name = format_value(field_name, json_api_settings.FORMAT_FIELD_NAMES)
                    if correct_field_name not in fieldset:
                        self.fields.pop(field_name)
```


## Checklist

- [x] Certain that this is a bug (if unsure or you have a question use [discussions](https://github.com/django-json-api/django-rest-framework-json-api/discussions) instead)
- [x] Code snippet or unit test added to reproduce bug



Discussion:
I feel like that would be reasonable. Perhaps using the `JSON_API_FORMAT_RELATION_KEYS` setting to reverse it to snake_case?

I suspect that it has to have an option to tell how to reverse the names... in case someone isn't following the pep8 standards.

Something like `JSON_API_PARSE_RELATION_KEYS`

Another way to solve this would be to use different names in the included_serializers.. although I am not quite sure if the names there have to match the field names!?

I think parsing it like you're doing in the PR is the right direction. The serializer doesn't strike me as the right way to stick this but I'm open to a counter argument

@schtibe @jerel I just found myself in your same situation :). However I have a question: should includes refer always to the `resource_name` of the resource you want to include? or should they refer to the field name you defined in the serializer?

Let me explain with some dummy (incomplete) code:

```
class Car(object):
   pass

class Passenger(object):
  passenger = ForeignKey(Car, related_name='human-passengers')


class PassengerSerializer(serializer.ModelSerializer):
   class Meta:
       model = Passenger

class CarSerializer(serializer.ModelSerializer):
   passengers = ResourceRelatedField(many=True, source='human-passengers', read_only=True)

   include_serializers = dict(passengers=PassengerSerializer)
   class Meta:
       model = Car

class Passenger(viewsets.ReadOnlyModelViewSet):
   resource_name = 'human-passengers'
   serializer_class = PassengerSerializer

class Car(viewsets.ReadOnlyModelViewSet):
   serializer_class = CarSerializer

```

Now If I did `GET /cars/1/?include=human-passengers` I'd expect the view to return the `human-passengers` resource included in the response (but it won't).

At the moment, to get this I'd have to do `GET /cars/1/?include=passengers`, because `passengers` matches the field name in `CarSerializer`.

Acording to:
http://jsonapi.org/format/#document-resource-objects
http://jsonapi.org/format/#fetching-includes

What you put in the includes should be the whatever the `type` key says. In this case, the `type` key for the `Passenger` resource should be `human-passengers` (because DRF jsonapi allows you to explicitely set `resource_name`).

This is a fairly old issue and there might be some confusion. So to clarify the specification states that the include query param are relationship field names and not resource names (see https://jsonapi.org/format/#fetching-includes).

So simply running the include name through the `utils.undo_format_field_name` will do it. There is already some `underscoring` of include names happening [here](https://github.com/django-json-api/django-rest-framework-json-api/blob/master/rest_framework_json_api/renderers.py#L288) but it does not take into consideration that includes maybe nested (e.g. include=author-bio.author-type)

This issue is related to #871 Maybe fixing both issue in one PR might be considered if the unformatting could happen in the utility method utils.get_included_resources).

Thanks for reporting this. When having a quick glance at the code it seems that this is indeed an issue.

Marking this as a bug. PR is very welcome.

For some who wants to have a look at this, [here](https://github.com/django-json-api/django-rest-framework-json-api/blob/master/rest_framework_json_api/views.py#L86) is the best spot to start.

I agree this is a bug. For compatibility as we do it in other spots it is better to run `undo_format_field_name` on the fieldset to solve this issue. PR is welcome.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3ec7ff7d..eae89c70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,11 @@ any parts of the framework not mentioned in the documentation should generally b
 * Added support for Django REST framework 3.16.
 * Added support for Django 5.2.
 
+### Fixed
+
+* Ensured that interpreting `include` query parameter is done in internal Python naming.
+  This adds full support for using multipart field names for includes while configuring `JSON_API_FORMAT_FIELD_NAMES`.
+
 ### Removed
 
 * Removed support for Python 3.8.
diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py
index 8c19934f..b670338f 100644
--- a/rest_framework_json_api/renderers.py
+++ b/rest_framework_json_api/renderers.py
@@ -6,7 +6,6 @@
 from collections import defaultdict
 from collections.abc import Iterable
 
-import inflection
 from django.db.models import Manager
 from django.template import loader
 from django.utils.encoding import force_str
@@ -277,9 +276,6 @@ def extract_included(
             current_serializer, "included_serializers", dict()
         )
         included_resources = copy.copy(included_resources)
-        included_resources = [
-            inflection.underscore(value) for value in included_resources
-        ]
 
         for field_name, field in iter(fields.items()):
             # Skip URL field
diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py
index d59dbd88..75764a5d 100644
--- a/rest_framework_json_api/serializers.py
+++ b/rest_framework_json_api/serializers.py
@@ -1,6 +1,5 @@
 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
@@ -129,7 +128,7 @@ def validate_path(serializer_class, field_path, path):
             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])
+            this_field_name = field_path[0]
             this_included_serializer = serializers.get(this_field_name)
             if this_included_serializer is None:
                 raise ParseError(
diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py
index 805f5f09..2dd79677 100644
--- a/rest_framework_json_api/utils.py
+++ b/rest_framework_json_api/utils.py
@@ -316,10 +316,18 @@ def get_resource_id(resource_instance, resource):
 
 
 def get_included_resources(request, serializer=None):
-    """Build a list of included resources."""
+    """
+    Build a list of included resources.
+
+    This method ensures that returned includes are in Python internally used
+    format.
+    """
     include_resources_param = request.query_params.get("include") if request else None
     if include_resources_param:
-        return include_resources_param.split(",")
+        return [
+            undo_format_field_name(include)
+            for include in include_resources_param.split(",")
+        ]
     else:
         return get_default_included_resources_from_serializer(serializer)
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 865244e0..77b3676b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,5 +1,5 @@
 import pytest
-from rest_framework.test import APIClient
+from rest_framework.test import APIClient, APIRequestFactory
 
 from tests.models import (
     BasicModel,
@@ -98,3 +98,8 @@ def nested_related_source(
 @pytest.fixture
 def client():
     return APIClient()
+
+
+@pytest.fixture
+def rf():
+    return APIRequestFactory()
diff --git a/tests/test_utils.py b/tests/test_utils.py
index a3beb12e..08e36b6a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -2,6 +2,7 @@
 from rest_framework import status
 from rest_framework.fields import Field
 from rest_framework.generics import GenericAPIView
+from rest_framework.request import Request
 from rest_framework.response import Response
 from rest_framework.views import APIView
 
@@ -13,6 +14,7 @@
     format_link_segment,
     format_resource_type,
     format_value,
+    get_included_resources,
     get_related_resource_type,
     get_resource_id,
     get_resource_name,
@@ -456,3 +458,22 @@ def test_get_resource_id(resource_instance, resource, expected):
 )
 def test_format_error_object(message, pointer, response, result):
     assert result == format_error_object(message, pointer, response)
+
+
+@pytest.mark.parametrize(
+    "format_type,include_param,expected_includes",
+    [
+        ("dasherize", "author-bio", ["author_bio"]),
+        ("dasherize", "author-bio,author-type", ["author_bio", "author_type"]),
+        ("dasherize", "author-bio.author-type", ["author_bio.author_type"]),
+        ("camelize", "authorBio", ["author_bio"]),
+    ],
+)
+def test_get_included_resources(
+    rf, include_param, expected_includes, format_type, settings
+):
+    settings.JSON_API_FORMAT_FIELD_NAMES = format_type
+
+    request = Request(rf.get("/test/", {"include": include_param}))
+    includes = get_included_resources(request)
+    assert includes == expected_includes