Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

function based views @api_view and request.POST (form based) parameters #279

Closed
axilaris opened this issue Jan 30, 2021 · 17 comments
Closed
Assignees
Labels
fix confirmation pending issue has been fixed and confirmation from issue reporter is pending

Comments

@axilaris
Copy link

axilaris commented Jan 30, 2021

I am going through a migration process from 1.11 to 3.1.5. And I found out drf-spectacular may provide a way to do this API migration.

I'ved got the question posted here in detail:
https://stackoverflow.com/questions/65918969/django-rest-framework-custom-post-url-endpoints-with-defined-parameter-request?noredirect=1#comment116622930_65918969

Basically, I'd like to find a way to create REST API using drf-spectacular that is equivalent to this:

url.py
url(r'^api/test_token$', api.test_token, name='test_token'),

api.py

@api_view(['POST'])
def test_token(request):
    # ----- YAML below for Swagger -----
    """
    description: test_token
    parameters:
      - name: token
        type: string
        required: true
        location: form       
    """
    token = request.POST['token']

    return Response("test_token success", status=status.HTTP_200_OK)

In the documentation, there is mentioned how to handle @api_view
https://drf-spectacular.readthedocs.io/en/latest/customization.html#

Many libraries use @api_view or APIView instead of ViewSet or GenericAPIView. In those cases, introspection has very little to work with. The purpose of this extension is to augment or switch out the encountered view (only for schema generation). Simply extending the discovered class class Fixed(self.target_class) with a queryset or serializer_class attribute will often solve most issues.

I am not sure how its done, could someone give some guidance how to get this to work ? Thanks.

@axilaris
Copy link
Author

Is OpenApiViewExtension a way to handle this with @api_view ? Can someone give an idea how to work this out ? Thanks. https://drf-spectacular.readthedocs.io/en/latest/customization.html#

@tfranzel
Copy link
Owner

hi @axilaris you refer to 2 different things.

  1. documenting your api. first of all we do not support docstring schema as shown above. we rely on decorators and you can decorate api_view with @extend_schema without any problem. this is also heavily used in the tests:
    @extend_schema(
    parameters=[OpenApiParameter(
    name='bbox',
    type={'type': 'array', 'minItems': 4, 'maxItems': 6, 'items': {'type': 'number'}},
    location=OpenApiParameter.QUERY,
    required=False,
    style='form',
    explode=False,
    )],
    responses=OpenApiTypes.OBJECT,
    )
    @api_view(['GET'])
    def view_func(request, format=None):
    pass # pragma: no cover
  2. documenting a library's api. this is done with extensions, as it is hard/inconvenient attaching decorators to code not under your control. it allows you to do 1. without actually touching the library files

the customization doc is quite thorough and the tests cover pretty much all use-cases.

@axilaris
Copy link
Author

axilaris commented Jan 31, 2021

@tfranzel Thank you very much, I think this is very helpful and the most straightforward migration way.

How can I find out more information on configuring the the parameter types in OpenApiParameter ?

In my case, I'd like to have an input parameter as a string of
'Content-Type: application/x-www-form-urlencoded'

@tfranzel
Copy link
Owner

spectacular is mostly self-explanatory given some basic knowledge about DRF and OpenApi3. There is also the documentation, and finally the tests describe every supported feature.

https://drf-spectacular.readthedocs.io/en/latest/customization.html#workflow-schema-customization
https://drf-spectacular.readthedocs.io/en/latest/drf_spectacular.html#drf_spectacular.utils.extend_schema

In my case, I'd like to have an input parameter as a string of
'Content-Type: application/x-www-form-urlencoded'

this sounds not like a parameter but a request body. however, if that is what you actually want there are ways of encoding parameters with style and explode.

@axilaris
Copy link
Author

axilaris commented Feb 1, 2021

Im sorry for my ignorance.

with drf_yasg, I can achieve the form based parameters in this way.

token = openapi.Parameter('token', openapi.IN_FORM, type=openapi.TYPE_STRING, required=True)
something = openapi.Parameter('something', openapi.IN_FORM, type=openapi.TYPE_INTEGER, required=False)
@swagger_auto_schema(
    method="post",
    manual_parameters=[token, something],
    operation_id="/v2/token_api"
)
@api_view(['POST'])

as for drf-spectacular, I manage to create swagger in this way but i am not clear where to define in_form parameter

@extend_schema( 
 parameters=[OpenApiParameter( 
     name='token', 
     type={'type': 'string'},  <--- i figure its somewhere here that needs to change to in_form string
     location=OpenApiParameter.QUERY,   <--- i figure its somewhere here that needs to change to in_form string
     required=False, 
     style='form', 
     explode=False, 
 )], 
 responses=OpenApiTypes.OBJECT, 
) 
@api_view(['POST'])
def test_token(request):

Could you provide some guidance ? I'm still interested to get drf-spectacular work. Thanks.

@tfranzel
Copy link
Owner

tfranzel commented Feb 1, 2021

i think i now get the confusion. drf_yasg.openapi.Parameter allows for form and body data. pretty sure that was added for compatibility reasons. we don't support it like that. parameters are either PATH, QUERY, COOKIE or HEADER. for request/response bodies you cannot use OpenApiParameter.

this is even discouraged in yasg: https://stackoverflow.com/questions/62930918/drf-yasg-swagger-setup-parameters-in-body-is-not-worked

this is likely what you want and is pretty much identical to yasg usage:

@extend_schema(request=OpenApiTypes.STR, responses=OpenApiTypes.STR)
@api_view(['POST'])
def test_token(request):
     ...

it you are mainly after application/x-www-form-urlencoded, it is generated from the parser_classes on the view. there is no override for request at the moment. for response it would be something like responses={(200, 'application/x-www-form-urlencoded'): OpenApiTypes.STR}

@axilaris
Copy link
Author

axilaris commented Feb 1, 2021

I tried this (like you suggest):

@extend_schema(request=OpenApiTypes.STR, responses=OpenApiTypes.STR)
@api_view(['POST'])
def test_token(request):

well, its pretty close but i guess its missing

--header 'Content-Type: application/x-www-form-urlencoded'

this is what it is generating now with drf-spectacular:

curl -X POST "http://localhost:8000/api/test_token/" -H "accept: application/json" -H "Content-Type: application/json" -H "X-CSRFToken: aewtJzKHxD8aoa7OopfP2bqBf7eTXiic2JltePPu5KXZi3adOgDtKhhhpsYXzGoL" -d "token=hello"

I am looking to generate this:

curl -X POST --header 'Content-Type: application/x-www-form-urlencoded' --header 'Accept: application/json' --header 'X-CSRFToken: aExHCSwrRyStDiOhkk8Mztfth2sqonhTkUFaJbnXSFKXCynqzDQEzcRCAufYv6MC' -d 'token=hello' 'http://localhost:8000/api/test_token/

Based on what you wrote, can you confirm drf-spectacular does not support this type of swagger document generation ?

Thanks

@tfranzel
Copy link
Owner

tfranzel commented Feb 2, 2021

drf-spectacular supports this in general, just not this particular override case. parser_classes are honored in Views, it just that you don't have direct access to it with @api_view

@tfranzel tfranzel self-assigned this Feb 11, 2021
@tfranzel
Copy link
Owner

extend_schema(request={'application/x-www-form-urlencoded': OpenApiTypes.OBJECT}) is now supported, i.e. overriding the default detetection not only for responses but also for the request side.

@tfranzel tfranzel added the fix confirmation pending issue has been fixed and confirmation from issue reporter is pending label Feb 23, 2021
@tfranzel tfranzel closed this as completed Mar 6, 2021
@romainr
Copy link

romainr commented Mar 7, 2021

Hello,

I have been digging quite a bit on how to provide a minimal POST json data API with specs and hitting the same question (despite all the great documentation and content, sorry if it is obvious).

Is there a way to extend_schema with a Request Body Object? (that way we could provide the schema of the body via the 'content' similarly to the examples in https://swagger.io/specification/#request-body-object. Or I am mistaken and there is no way to actually provide a schema for the request body)

Or, by design should the schema of the json body be defined by a serializer and provided to the extend_schema?
https://www.django-rest-framework.org/api-guide/serializers/#declaring-serializers
And if yes, is there a lighter weight way of providing the schema instead of Python? (e.g. via text)

A compromise might also be to drop trying to type the json body and provide an OpenApiExample with a json string instead.

  1. I would volunteer to add the recommended solution to the FAQ https://drf-spectacular.readthedocs.io/en/latest/faq.html ;)

I saw nowadays many API not using parameters anymore but json body instead, e.g.
Twitter API v2 is using a json body in POST (e.g. following, while v1 was using header parameters (e.g. post tweet).

Thanks!

@tfranzel
Copy link
Owner

tfranzel commented Mar 7, 2021

hi,

you cannot explicitly replace request body object, but you can get very close with this (1&2):

@extend_schema(
    examples=[
        OpenApiExample('Serializer C Example RO',value={"field": 111})
    ],
    responses={
        (200, 'application/json'): OpenApiTypes.OBJECT,  # or anything else
    }
    request={
       'application/json': OpenApiTypes.OBJECT
    },
)

you can also manually specify a schema. there several ways to override the response and raw dict is one of them. you have complete freedom there (2):

@extend_schema(operation={
"operationId": "manual_endpoint",
"description": "fallback mechanism where can go all out",
"tags": ["manual_tag"],
"requestBody": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Alpha"}
},
}
},
"deprecated": True,
"responses": {
"200": {
"content": {
"application/json": {
"schema": {"$ref": "#/components/schemas/Gamma"}
}
},
"description": ""
},
}
})
@action(detail=False, methods=['POST'])
def manual(self, request):
return Response() # pragma: no cover

of course the more manual you go, you alos loose a lot of benefits. i hope this answers your question

@romainr
Copy link

romainr commented Mar 8, 2021

Thanks a lot @tfranzel !
Playing with the options and will document what a fair compromise could be (basically how much "manual" is worth it)

@AJ-BM
Copy link

AJ-BM commented Jun 9, 2021

Hey @tfranzel. First, thanks for the great library :)

Have been trying for days now to get this manual override to work without luck. My initial idea was to do like this:

extend_schema(operation={
    "operationId": "upload_endpoint",
    "description": "fallback mechanism where we can go all out",
    "tags": ["manual_tag"],
    "requestBody": {
        "content": {
            "application/json": {
                "schema": {"$ref": "#/components/requestBodies/Route"}
            },
        },
    },
    "responses": {
        "200": {
            "content": {
                "application/json": {
                    "schema": {"$ref": "#/components/schemas/RouteV3Model"}
                }
            },
        },
    }
})
@action(detail=false, methods=["POST"])
def upload(self, request, *args, **kwargs):

But this does nothing. It just fails silently without telling me whats wrong and the endpoint ends up having its normal description, request etc, without any of the data specified in the extended schema annotation. Next thing i tried was to specify the response and the request body directly in the extended schema annotation but this also did nothing. So:

  1. Why does this fail ?

  2. Where exactly is the schema.yaml supposed to be located in the project and is there anything special i need to do before it can be referenced from the extended schema annotations ? (In my Django project i have it located directly in the src folder which is where it was generated when i ran the command ./manage.py spectacular --file schema.yaml --validate --fail-on-warn)

  3. In the normal non-manual way, that you posted on the 7th of march in this thread isn't there a way to reference the components section in the schema.yaml file? Something like below:

@extend_schema(
examples=[
{"$ref": "#/components/examples/MyExample"}
],
responses={
{"$ref": "#/components/schemas/MyModel"}
}
request={
{"$ref": "#/components/requestBodies/MyRequestBody"} #(this request body uses the MyExample and the MyModel)
},
)

@tfranzel
Copy link
Owner

tfranzel commented Jun 9, 2021

@AndersJensenBikemap, i moved your question to #421 as it looks orthogonal to this issue.

@xavierrigau
Copy link

xavierrigau commented Dec 15, 2021

To specify the body the approach of:

  1. Using serializers in python: Doesn't scale well with number of parameters and complexity (references, nesting, etc)

  2. Using operation= is too manual. It is almost like using the whole yaml/json definition of the API. There are too many things required when only the schema is needed.

  3. My preference would be to only pass the schema for the body. Like: {"application/json": OpenApiSchema} much like the old:

   @extend_schema(
        request_body=openapi.Schema(
            in_=openapi.IN_BODY,
            type=openapi.TYPE_OBJECT,
            properties={
                "long_url": openapi.Schema(
                    type=openapi.TYPE_STRING,
                    format=openapi.FORMAT_URI,
                )
            },
        ),
    )

@tfranzel
Copy link
Owner

tfranzel commented Dec 15, 2021

@xavierrigau, the structure has to come from somewhere. you don't need to create serializers for one-off usages. do this instead:

    @extend_schema(
        request=inline_serializer(
            name='InlineOneOffSerializer',
            fields={
                'long_url': serializers.URLField(),
            }
        )
    )

   # or with explicit content type override (usually not needed)

    @extend_schema(
        request={
            'application/json': inline_serializer(...),
        }     
    )

My preference would be to only pass the schema for the body

we have a very consistent API for this. you have to let go of your old drf-yasg patterns to make full use of spectacular.

@xavierrigau
Copy link

@xavierrigau, the structure has to come from somewhere. you don't need to create serializers for one-off usages. do this instead:

    @extend_schema(
        request=inline_serializer(
            name='InlineOneOffSerializer',
            fields={
                'long_url': serializers.URLField(),
            }
        )
    )

   # or with explicit content type override (usually not needed)

    @extend_schema(
        request={
            'application/json': inline_serializer(...),
        }     
    )

My preference would be to only pass the schema for the body

we have a very consistent API for this. you have to let go of your old drf-yasg patterns to make full use of spectacular.

Is it possible to create a serializer from a json schema string?

Danke schön

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
fix confirmation pending issue has been fixed and confirmation from issue reporter is pending
Projects
None yet
Development

No branches or pull requests

5 participants