tfranzel / drf-spectacular

Sane and flexible OpenAPI 3 schema generation for Django REST framework.
https://drf-spectacular.readthedocs.io
BSD 3-Clause "New" or "Revised" License
2.38k stars 264 forks source link

Unable to override discovered serializer when dj_rest_auth is in use #748

Closed marlier closed 2 years ago

marlier commented 2 years ago

Description

I have a custom Registration view, which derives from dj_rest_auth's RegisterView. The create() method is over-ridden, and takes and returns custom objects, which have appropriate serializers.

I have annotated the create() method with @extend_schema, but neither the request= nor responses={...} parameters are being respected.

To Reproduce The code looks like this:

class LinkingRegisterView(RegisterView):
    serializer_class = RedirectingJWTSerializerWithExpiration

    @extend_schema(
        methods=["POST"],
        request=RegisterSerializerWithAccountId,
        responses={201: RedirectingJWTSerializerWithExpiration},
    )
    def create(self, request, *args, **kwargs):
        .....

On running python manage.py spectacular --file openapi-schema.json, the following openapi schema is generated:

  /auth/registration/:
    post:
      operationId: auth_registration_create
      tags:
      - auth
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedirectingJWTSerializerWithExpiration'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/RedirectingJWTSerializerWithExpiration'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/RedirectingJWTSerializerWithExpiration'
        required: true
      security:
      - jwtHeaderAuth: []
        jwtCookieAuth: []
      - cookieAuth: []
      - tokenAuth: []
      - {}
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectingJWTSerializerWithExpiration'
          description: ''

As you can see, the serializer provided in the @extend_schema request= parameter is not being respected.

If I instead set serializer_class to the body parameter value:

class LinkingRegisterView(RegisterView):
    serializer_class = RegisterSerializerWithAccountId

    @extend_schema(
        methods=["POST"],
        request=RegisterSerializerWithAccountId,
        responses={201: RedirectingJWTSerializerWithExpiration},
    )
    def create(self, request, *args, **kwargs):
        .....

Then the resulting schema is:

  /auth/registration/:
    post:
      operationId: auth_registration_create
      tags:
      - auth
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterSerializerWithAccountId'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/RegisterSerializerWithAccountId'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/RegisterSerializerWithAccountId'
        required: true
      security:
      - jwtHeaderAuth: []
        jwtCookieAuth: []
      - cookieAuth: []
      - tokenAuth: []
      - {}
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegisterSerializerWithAccountId'
          description: ''

Finally, if I remove the serializer_class parameter entirely:

class LinkingRegisterView(RegisterView):
    @extend_schema(
        methods=["POST"],
        request=RegisterSerializerWithAccountId,
        responses={201: RedirectingJWTSerializerWithExpiration},
    )
    def create(self, request, *args, **kwargs):
        .....

the generated schema uses the Register serializer, which is what is set on the dj_rest_auth RegisterView:

  /auth/registration/:
    post:
      operationId: auth_registration_create
      tags:
      - auth
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Register'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/Register'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/Register'
        required: true
      security:
      - jwtHeaderAuth: []
        jwtCookieAuth: []
      - cookieAuth: []
      - tokenAuth: []
      - {}
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Register'
          description: ''

Expected behavior Expected behavior is that the settings in @extend_schema will be respected, meaning that this code:

class LinkingRegisterView(RegisterView):
    serializer_class = RedirectingJWTSerializerWithExpiration

    @extend_schema(
        methods=["POST"],
        request=RegisterSerializerWithAccountId,
        responses={201: RedirectingJWTSerializerWithExpiration},
    )
    def create(self, request, *args, **kwargs):
        .....

Will result in this schema:

  /auth/registration/:
    post:
      operationId: auth_registration_create
      tags:
      - auth
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterSerializerWithAccountId'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/RegisterSerializerWithAccountId'
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/RegisterSerializerWithAccountId'
        required: true
      security:
      - jwtHeaderAuth: []
        jwtCookieAuth: []
      - cookieAuth: []
      - tokenAuth: []
      - {}
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedirectingJWTSerializerWithExpiration'
          description: ''
tfranzel commented 2 years ago

Hi,

you threw me off there for a second. So if I get it right everything in @extend_schema is ignored and only the attr serializer_class is considered. the core issue is this:

https://drf-spectacular.readthedocs.io/en/latest/faq.html#using-extend-schema-on-apiview-has-no-effect

so change create to post and it will work.

fyi: this is a third party lib and thus we provide functionality through extensions. Though they do not trigger on subclasses by default, but generally extensions do take precedence over decoration. Since you are subclassing, decoration is fine but you might want to have a look at how the response serializer is estimated. Also notice that we decorate the post in the extension for exactly the same reason as above.

marlier commented 2 years ago

Gotcha -- that makes sense, and adding a post method (that just calls super().post()) with the annotation did, indeed, fix it. Appreciate the help!