Djangorestframework-simplejwt: Issue with schema generation

Created on 13 May 2020  ·  6Comments  ·  Source: jazzband/djangorestframework-simplejwt

Hello folks!
I've used this library in a lot of projects, and I very like its simplicity and effectivity.

Issue

Recently I've mentioned the small issue - default schema generation can't recognize response fields.

Part of generated OpenAPI schema for the Token Obtain request:

requestBody:
  content:
    application/json:
      schema: &id001
        properties:
          username:
            type: string
          password:
            type: string
            writeOnly: true
        required:
        - username
        - password
    application/x-www-form-urlencoded:
      schema: *id001
    multipart/form-data:
      schema: *id001
responses:
  '200':
    content:
      application/json:
        schema:
          properties:
            username:
              type: string
          required:
          - username
    description: ''

As can be seen, the schema states that the response contains only the username field, but actually, it should return other fields: access and refresh.

Possible Solution

I've got a look at the sources and found that fields access and refresh are not mentioned in the serializer. Probably, we can mark username and password fields as write_only, and add access and refresh fields as read_only. This should make a trick.

bug

Most helpful comment

Here's a simple workaround if you're using drf-yasg:

@method_decorator(
    name='post',
    decorator=swagger_auto_schema(
        responses={
            status.HTTP_200_OK: openapi.Schema(
                title='TokenPair',
                type='object',
                properties={
                    'refresh': openapi.Schema(title='Refresh token', type='string'),
                    'access': openapi.Schema(title='Access token', type='string'),
                },
                required=['access', 'refresh']
            )
        },
    )
)
class MyTokenView(TokenObtainPairView):
    pass


@method_decorator(
    name='post',
    decorator=swagger_auto_schema(
        responses={
            status.HTTP_200_OK: openapi.Schema(
                title='AccessToken',
                type='object',
                properties={
                    'access': openapi.Schema(title='Access token', type='string')
                },
                required=['access']
            )
        }
    )
)
class MyRefreshView(TokenRefreshView):
    pass

All 6 comments

Are you saying we should add the username to the response? The TokenObtainPairView should only be a POST HTTP method. The serializer does not include access and refresh because it uses the validate method in the view (i.e. using serializer.validated_data) to make new data, as you can see here: https://github.com/SimpleJWT/django-rest-framework-simplejwt/blob/ce80c2d31f0d9aff6a246d30d9aaa27f1e57a364/rest_framework_simplejwt/serializers.py#L70

On L71, you get an empty dict if the authentication was successful, as can be seen here: https://github.com/SimpleJWT/django-rest-framework-simplejwt/blob/ce80c2d31f0d9aff6a246d30d9aaa27f1e57a364/rest_framework_simplejwt/serializers.py#L58

So, in theory, the access and refresh token are read only and the user credentials are still write only.

Thank you for your reply.

I wanted to say that the current behavior is that the response contains two fields - access and refresh, but DRF's automatically generated schema states the response field is username (which is not true).

It is a bit confusing (in my cases) for front-end developers when they look at the generated schema and see invalid response fields.

Also, I suggested that explicitly adding access and refresh fields to the serializer could do the trick. I can check this suggestion a bit later.

I see. I do mostly full stack, so I generally just write some documentation of the response json. I suppose it's possible we can change the serializer's behavior, which would probably mean fragmenting that main serializer.

@regzon Sorry it's been awhile. Taking a retrospective look at serializers.py, the way TokenObtainSerializer is setup with the __init__ magic method has to be like that due to a dynamic self.username_field field name. If we made a class name stating User.USERNAME_FIELD = serializers.CharField(). It may not work. I'll write some test case at some point to see if it's feasible to do that, but I slightly doubt it.

Here's a simple workaround if you're using drf-yasg:

@method_decorator(
    name='post',
    decorator=swagger_auto_schema(
        responses={
            status.HTTP_200_OK: openapi.Schema(
                title='TokenPair',
                type='object',
                properties={
                    'refresh': openapi.Schema(title='Refresh token', type='string'),
                    'access': openapi.Schema(title='Access token', type='string'),
                },
                required=['access', 'refresh']
            )
        },
    )
)
class MyTokenView(TokenObtainPairView):
    pass


@method_decorator(
    name='post',
    decorator=swagger_auto_schema(
        responses={
            status.HTTP_200_OK: openapi.Schema(
                title='AccessToken',
                type='object',
                properties={
                    'access': openapi.Schema(title='Access token', type='string')
                },
                required=['access']
            )
        }
    )
)
class MyRefreshView(TokenRefreshView):
    pass

Here's a simple workaround if you're using drf-yasg:

@method_decorator(
    name='post',
    decorator=swagger_auto_schema(
        responses={
            status.HTTP_200_OK: openapi.Schema(
                title='TokenPair',
                type='object',
                properties={
                    'refresh': openapi.Schema(title='Refresh token', type='string'),
                    'access': openapi.Schema(title='Access token', type='string'),
                },
                required=['access', 'refresh']
            )
        },
    )
)
class MyTokenView(TokenObtainPairView):
    pass


@method_decorator(
    name='post',
    decorator=swagger_auto_schema(
        responses={
            status.HTTP_200_OK: openapi.Schema(
                title='AccessToken',
                type='object',
                properties={
                    'access': openapi.Schema(title='Access token', type='string')
                },
                required=['access']
            )
        }
    )
)
class MyRefreshView(TokenRefreshView):
    pass

i am using drf-yasg。
And some imperfections
thinks, Inspired me。

@Andrew-Chen-Wang @regzon @mithodin
This is my ultimately correct:
created xxx/xxx.py:

from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

@method_decorator(
    name='post',
    decorator=swagger_auto_schema(
        operation_summary='授权获取',
        operation_description='使用账号密码获取token',
        request_body=openapi.Schema(
            type=openapi.TYPE_OBJECT,
            properties={
                'username': openapi.Schema(
                    type=openapi.TYPE_STRING,
                    description='用户名'
                ),
                'password': openapi.Schema(
                    type=openapi.TYPE_STRING,
                    description='密码'
                )
            }
        ),
        responses={
            "200": openapi.Response(
                description="success!",
                schema=openapi.Schema(
                    type=openapi.TYPE_OBJECT,
                    properties={
                        'access': openapi.Schema(
                            type=openapi.TYPE_STRING,
                            description='身份token'
                        ),
                        'refresh': openapi.Schema(
                            type=openapi.TYPE_STRING,
                            description='刷新token的refresh'
                        ),
                    }
                ),
            ),

        }
    )
)
class MyTokenView(TokenObtainPairView):
    pass


@method_decorator(
    name='post',
    decorator=swagger_auto_schema(
        operation_summary='授权刷新',
        operation_description='token过期后,使用refresh刷新token',
        request_body=openapi.Schema(
            type=openapi.TYPE_OBJECT,
            properties={
                'refresh': openapi.Schema(
                    type=openapi.TYPE_STRING,
                    description='refresh'
                )
            }
        ),
        responses={
            "200": openapi.Response(
                description="success!",
                schema=openapi.Schema(
                    type=openapi.TYPE_OBJECT,
                    properties={
                        'access': openapi.Schema(
                            type=openapi.TYPE_STRING,
                            description='身份token'
                        ),
                        'refresh': openapi.Schema(
                            type=openapi.TYPE_STRING,
                            description='刷新token的refresh'
                        ),
                    }
                ),
            ),

        }
    )
)
class MyRefreshView(TokenRefreshView):
    pass

url.py

from xxx.xxx import MyTokenView, MyRefreshView


urlpatterns = [

    path('auth/token/', MyTokenView.as_view(), name='token_obtain_pair'),
    path('auth/token/refresh/', MyRefreshView.as_view(), name='token_refresh'),

]

Was this page helpful?
0 / 5 - 0 ratings