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

Issue:
Incorrect handling of `sort` field in schema generation
## Description of the Bug Report

I believe that the handling of the `sort` field in the schema generation is currently incorrect. (Please correct me if I'm wrong.)

The custom `AutoSchema` class calls [`self._get_sort_parameters(path, method)`]((https://github.com/django-json-api/django-rest-framework-json-api/blob/085554e28c663d9a336bf7174c58d81925d9d650/rest_framework_json_api/schemas/openapi.py#L425)) for all `GET` & `HEAD` requests:

```python
if method in ["GET", "HEAD"]:
    ...
    parameters += self._get_sort_parameters(path, method)
    ...
```

(_Issue 1_) The JSON:API spec says, "A server **MAY** choose to support requests to sort resource _collections_ according to one or more criteria (“sort fields”)." The way that this is currently handled will naively add the sort query parameter irrespective of whether or not `rest_framework_json_api.filters.OrderingFilter` is enabled. Also, this will add a `sort` query parameter for requests for single resources as well (though I think DRF also does this).

(_Issue 2_) The Django REST Framework's `OrderingFilter` class implements a method for `get_schema_operation_parameters` that will also [add a `sort` query parameter to the schema for the operation](https://github.com/encode/django-rest-framework/blob/0618fa88e1a8c2cf8a2aab29ef6de66b49e5f7ed/rest_framework/filters.py#L322). This leads to duplicate `sort` query parameters listed for the operation.

```python
def get_schema_operation_parameters(self, view):
        return [
            {
                'name': self.ordering_param,
                'required': False,
                'in': 'query',
                'description': force_str(self.ordering_description),
                'schema': {
                    'type': 'string',
                },
            },
        ]
``` 

## Current Workaround

I've currently worked around this by overriding the `OrderingFilter` class:
```python
from rest_framework_json_api.filters import OrderingFilter as DJAOrderingFilter

class OrderingFilter(DJAOrderingFilter):
    def get_schema_operation_parameters(self, view):
        return [{"$ref": "#/components/parameters/sort"}]
```

...and the `AutoSchema` class:
```python
from rest_framework_json_api.schemas.openapi import AutoSchema as DJAAutoSchema

class AutoSchema(DJAAutoSchema):
        def _get_sort_parameters(self, path, method):
            return []
```

...and configuring these in my `REST_FRAMEWORK` config so that the `sort` parameter is conditionally added based on whether or not the `OrderingFilter` is enabled, but still uses the schema provided by this lib. Just thought I'd include this as a potential fix to the problem. However, this patch will still include a `sort` query parameter for requests for single resources (mentioned above).



Discussion:
Thanks for your report. I agree this is a bug. The difference between `OrderingFilter.get_schema_operation_parameters` and `#/components/parameters/sort` is just the description, it seems. Therefore, I am asking myself, is it worth creating our own `OrderingFiltering` class with just another description? I think the easiest way would be to simply remove `_get_sort_parameters` and remove reference `#/components/parameters/sort` to address this issue. What are your thoughts?
Of course. Happy to help out. Thanks for the great work on this lib.

Yeah, I think you're right about the two `sort` specs being more or less the same. DRF allows you to override the description by setting the `ordering_description` on the filter, so that isn't an issue either. 

However, there's already an [`OrderingFilter`](https://github.com/django-json-api/django-rest-framework-json-api/blob/c46d495e38bf46c09277ce34230b966a4cd0ec1d/rest_framework_json_api/filters.py#L9) class that extends DRF's `OrderingFilter` class, which is imposing some additional validation logic on top of what DRF does.

That being said, my initial thinking would be to: 

1. Remove the [`_get_sort_parameters` method in the `SchemaGenerator` class](https://github.com/django-json-api/django-rest-framework-json-api/blob/9ced3be23c5b5c7772d11959148e350300e382d0/rest_framework_json_api/schemas/openapi.py#L488) (as well as [this line](https://github.com/django-json-api/django-rest-framework-json-api/blob/9ced3be23c5b5c7772d11959148e350300e382d0/rest_framework_json_api/schemas/openapi.py#L425))
2. Add the following method to the custom `OrderingFilter` class to conditionally render the sort field schema operation parameters since I'm not sure if abiding by the spec constraint of only including `sort` for collection resources will be prioritized (or even acknowledged as a bug) by the DRF contributors:
```python
def get_schema_operation_parameters(self, view):
    # ViewSets have an explicitly defined action, which we can inspect
    if getattr(view, 'action', None) != 'list':
        return []
    else:
        # Return the ordering schema operation parameters for resource collections only
        return super().get_schema_operation_parameters(view)
```

**Note 1:** I've used `super().get_schema_operation_parameters(view)` instead of `[{"$ref": "#/components/parameters/sort"}]` in the `return` above given your observation that the two `sort` specs are pretty much the same. If we do that, then we can also get rid of the corresponding schema definition [here](https://github.com/django-json-api/django-rest-framework-json-api/blob/9ced3be23c5b5c7772d11959148e350300e382d0/rest_framework_json_api/schemas/openapi.py#L251).

**Note 2:** The above `if` statement naively determines if this is a list view by exploiting the fact that `ViewSets` are used exclusively in this lib when listing collection resources (I think - but let me know if I'm mistaken). We can't use the DRF `is_list_view` util (`from rest_framework.schemas.utils import is_list_view`) because we don't have access to the method or path in `get_schema_operation_parameters`.

Let me know what you think.
1. I fully agree with this point. `_get_sort_parameters` needs to be removed with the scheme definition. Additionally, I would set the `ordering_description` to what we use today in the corresponding scheme definition. Would you be open to work on a PR for this?
2. This point is a bit more tricky. The reason is that simply removing sort parameter from the schema for single resource endpoints actually does not mean that the sort parameter cannot be used on single resource object endpoints. And when it is used, no error is thrown and the query to the database will actually be different. I haven't had time yet to look deeper into this, but I have a feeling that an error needs to be thrown when query param `sort` is used on a single resource object. We have to be careful though in terms of regressions.

I recommend we first tackle point 1 and discuss point 2 more deeply afterward.

Resolving patch:
diff --git a/rest_framework_json_api/filters.py b/rest_framework_json_api/filters.py
--- a/rest_framework_json_api/filters.py
+++ b/rest_framework_json_api/filters.py
@@ -22,6 +22,9 @@ class OrderingFilter(OrderingFilter):
     #: override :py:attr:`rest_framework.filters.OrderingFilter.ordering_param`
     #: with JSON:API-compliant query parameter name.
     ordering_param = "sort"
+    ordering_description = (
+        "[list of fields to sort by]" "(https://jsonapi.org/format/#fetching-sorting)"
+    )
 
     def remove_invalid_fields(self, queryset, fields, view, request):
         """
diff --git a/rest_framework_json_api/schemas/openapi.py b/rest_framework_json_api/schemas/openapi.py
--- a/rest_framework_json_api/schemas/openapi.py
+++ b/rest_framework_json_api/schemas/openapi.py
@@ -248,15 +248,6 @@ class SchemaGenerator(drf_openapi.SchemaGenerator):
                 },
                 "explode": True,
             },
-            "sort": {
-                "name": "sort",
-                "in": "query",
-                "description": "[list of fields to sort by]"
-                "(https://jsonapi.org/format/#fetching-sorting)",
-                "required": False,
-                "style": "form",
-                "schema": {"type": "string"},
-            },
         },
     }
 
@@ -422,7 +413,6 @@ def get_operation(self, path, method):
         if method in ["GET", "HEAD"]:
             parameters += self._get_include_parameters(path, method)
             parameters += self._get_fields_parameters(path, method)
-            parameters += self._get_sort_parameters(path, method)
             parameters += self.get_pagination_parameters(path, method)
             parameters += self.get_filter_parameters(path, method)
         operation["parameters"] = parameters
@@ -485,12 +475,6 @@ def _get_fields_parameters(self, path, method):
         # explode: true
         return [{"$ref": "#/components/parameters/fields"}]
 
-    def _get_sort_parameters(self, path, method):
-        """
-        sort parameter: https://jsonapi.org/format/#fetching-sorting
-        """
-        return [{"$ref": "#/components/parameters/sort"}]
-
     def _add_get_collection_response(self, operation, path):
         """
         Add GET 200 response for a collection to operation


Test patch:
diff --git a/example/tests/__snapshots__/test_openapi.ambr b/example/tests/__snapshots__/test_openapi.ambr
--- a/example/tests/__snapshots__/test_openapi.ambr
+++ b/example/tests/__snapshots__/test_openapi.ambr
@@ -273,10 +273,7 @@
         "$ref": "#/components/parameters/fields"
       },
       {
-        "$ref": "#/components/parameters/sort"
-      },
-      {
-        "description": "Which field to use when ordering the results.",
+        "description": "[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)",
         "in": "query",
         "name": "sort",
         "required": false,
@@ -391,9 +388,6 @@
       {
         "$ref": "#/components/parameters/fields"
       },
-      {
-        "$ref": "#/components/parameters/sort"
-      },
       {
         "description": "A page number within the paginated result set.",
         "in": "query",
@@ -413,7 +407,7 @@
         }
       },
       {
-        "description": "Which field to use when ordering the results.",
+        "description": "[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)",
         "in": "query",
         "name": "sort",
         "required": false,
diff --git a/example/tests/unit/test_filter_schema_params.py b/example/tests/unit/test_filter_schema_params.py
--- a/example/tests/unit/test_filter_schema_params.py
+++ b/example/tests/unit/test_filter_schema_params.py
@@ -72,7 +72,8 @@ def test_filters_get_schema_params():
                     "name": "sort",
                     "required": False,
                     "in": "query",
-                    "description": "Which field to use when ordering the results.",
+                    "description": "[list of fields to sort by]"
+                    "(https://jsonapi.org/format/#fetching-sorting)",
                     "schema": {"type": "string"},
                 }
             ],