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.33k stars 259 forks source link

drf-spectacular does not detect any schema #364

Closed RomikimoR closed 3 years ago

RomikimoR commented 3 years ago

Hello,

I am trying to generate an open Api documentation using your library, however I keep getting the No operations defined in spec! while using the ui. I believe it is an error due to my own implementation.

All my view can be find like this:

my app / views / view1.py view2.py

I mostly use the generics views, here is an example:

class SystemDetail(generics.RetrieveAPIView):
""" Get a system detail
User in group x can see all system.
Other groups have access to system with the corresponding dealer name.

For now it return Not Found id Not Authorized.
"""

lookup_field = 'uuid'

def get_queryset(self):
    user_groups = self.request.user.groups.all().values_list('name', flat=True)

    if 'x' not in user_groups:
        dealer = MANUFACTER_CHOICE
        dealer_name: str = [dealer_name[1] for dealer_name in dealer if dealer_name[1].lower() in user_groups][0]
        return System.objects.filter(dealer__name=dealer_name)
    return System.objects.all()

def get_serializer_class(self):
    user_groups = self.request.user.groups.all().values_list('name', flat=True)

    if 'x' not in user_groups:
        return ExternalSystemSerializer
    return SystemSerializer

I set up drf-spectacular using the docs. I added it to my INSTALLED_APPS in my settings.py and in my urls.

urlpatterns = [
# back
path('', include('api.urls')),
path('schema/', SpectacularAPIView.as_view(), name='schema'),
# Optional UI:
path('schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
path('v1/', include('users.urls.v1', namespace='users')),]

All my endpoint are served under my-domain-name/v1/my-endpoint.

When I try to see my swagger It returns an empty dict as if I had no views in my project, however when I was using drf-yasg I could see all my endopints.

I tried to add this to my settings.py, same result.

SPECTACULAR_SETTINGS = { "SCHEMA_PATH_PREFIX": "/v1" }

Where Am I doing wrong ?

Thank you

tfranzel commented 3 years ago

Hi @RomikimoR, from your examples i can't really tell how/if you are using versioning but this is the problem in 99% of the cases where the schema is completely empty without warnings.

https://drf-spectacular.readthedocs.io/en/latest/faq.html#i-get-an-empty-schema-or-endpoints-are-missing

the schema view is unversioned by default and if all views are versioned, the schema is empty.

you can do 2 things:

  1. nest the schema endpoints under the v1/schema/, then the version should be used automatically (given the default versioning class is correct)
  2. explicitly provide version to schema views: SpectacularAPIView.as_view(api_version='v1')
  3. similar to 2., the CLI can also be provided with explicit version
RomikimoR commented 3 years ago

Hi,

Ok I am sorry about opening an issur for such an easy problem I did not get that namespace are used for versioning.

Have a great day.

arielpontes commented 3 years ago

I'm working on a project in which all API endpoints are in the namespace /api/1.0/. I tried to generate the Swagger UI as follows:

urlpatterns = [
    path("api/1.0/", include((router.urls + bmu_router.urls, "api"))),
    # ...
    # Schema
    path("schema/", SpectacularAPIView.as_view(api_version="api/1.0"), name="schema"),
    # Swagger UI:
    path(
        "swagger/",
        SpectacularSwaggerView.as_view(url_name="schema"),
        name="swagger-ui",
    ),
    path(
        "swagger/redoc/",
        SpectacularRedocView.as_view(url_name="schema"),
        name="redoc",
    ),
]

I still get No operations defined in spec!. Am I doing something wrong?

tfranzel commented 3 years ago

hi! i suppose your route is /api/1.0, but the namespace is probably 1.0, so you would need to set api_version="1.0"

otherwise what you did there looks good. you can also have a look at the tests, which are designed to also serve as examples: https://github.com/tfranzel/drf-spectacular/blob/master/tests/test_versioning.py

arielpontes commented 3 years ago

Hi, I updated my previous comment with more information. As far as I understand, the namespace is api, not 1.0 (not sure this makes sense but it's the code I was given). Still, I've tried passing everything as api_version: api, 1.0, api/1.0, nothing works.

arielpontes commented 3 years ago

Oops, just found my error, I had another VERSION set in SPECTACULAR_SETTINGS. I commented it out and now it works :) thanks for the help!

abhinavsingh commented 2 years ago

Hi,

Looking at the doc for this error, I suspected it must be an auth issue, because our API uses it's own auth mechanism. I was able to hack into template js to add necessary code for auth. But I am still getting No operations defined in spec!.

Schema is empty. What am I missing here? I have tried a few flavors of below:

path(
        "v1/spectacular-schema/",
        SpectacularAPIView.as_view(api_version="v1"),
        name="spectacular-schema",
    ),
path(
        "spectacular-schema/",
        SpectacularAPIView.as_view(api_version="v1"),
        name="spectacular-schema",
    ),

Adding api_version="v1" doesn't seem to help. Similarly SPECTACULAR_SETTINGS didn't made any change either.

tfranzel commented 2 years ago

I suspected it must be an auth issue, because our API uses it's own auth mechanism.

pretty sure this unrelated unless you use changed SERVE_PUBLIC.

I was able to hack into template js to add necessary code for auth.

that seems weird and not required unless you do some really funky stuff. otherwise extensions are the way to go.

api_version="v1" does work for sure, your views however might not successfully register as versioned views or "v1" is not the right string for your case. this is almost certainly a view configuration issue on your side. check the version inside your view at runtime and compare

abhinavsingh commented 2 years ago

SERVE_PUBLIC

that seems weird and not required unless you do some really funky stuff. otherwise extensions are the way to go.

Agreed. We have our own mechanism under which a credentials.json is downloaded by the client. Then dynamically, a web access token is generated using the credentials file for any Ajax request. Along with token, access key must also be passed. So, in gist, 2 headers needs to be passed to access API or schema itself

I can confirm auth works after the hack I made, because necessary headers were present in outgoing schema fetch requests.

otherwise extensions are the way to go.

Agreed again. I would love to start using an extension to achieve this.

api_version="v1" does work for sure, your views however might not successfully register as versioned views or "v1" is not the right string for your case. this is almost certainly a view configuration issue on your side. check the version inside your view at runtime and compare

Value for our DEFAULT_VERSIONING_CLASS is None. Does that impact spectacular integration? Also what version are you asking me to check at runtime, schema version or API version?

abhinavsingh commented 2 years ago

We have our own mechanism under which a credentials.json is downloaded by the client. Then dynamically, a web access token is generated using the credentials file for any Ajax request. Along with token, access key must also be passed. So, in gist, 2 headers needs to be passed to access API or schema itself

For posterity, to achieve the above I did the following:


class AuthSpectacularSwaggerView(SpectacularSwaggerView):
    template_name_js = "core/api/swagger-ui-js.html"

    @extend_schema(exclude=True)
    def get(self, request, *args, **kwargs):
        return Response(
            data={
                "title": self.title,
                "dist": self._swagger_ui_dist(),
                "favicon_href": self._swagger_ui_favicon(),
                "schema_url": self._get_schema_url(request),
                "settings": self._dump(spectacular_settings.SWAGGER_UI_SETTINGS),
                "oauth2_config": self._dump(
                    spectacular_settings.SWAGGER_UI_OAUTH2_CONFIG
                ),
                "template_name_js": self.template_name_js,
                "csrf_header_name": self._get_csrf_header_name(),
                "schema_auth_names": self._dump(self._get_schema_auth_names()),
                "api_key": extra_context["api_key"],
                "api_token": extra_context["api_token"],
            },
            template_name=self.template_name,
        )

All I needed was to pass additional context which is then used within the js file as:

  request.headers['X-API-KEY'] = "{{ api_key }}"
  request.headers['X-API-TOKEN'] = "{{ api_token }}"
abhinavsingh commented 2 years ago

I guess we need to fix schema generation.

> curl http://localhost:8000/spectacular-schema/                                                                   ─╯
openapi: 3.0.3
info:
  title: ''
  version: 0.0.0
paths: {}
components: {}

^^^ Currently returns empty. Schema path is set as:

path(
        "spectacular-schema/",
        SpectacularAPIView.as_view(),
        name="spectacular-schema",
    ),

I currently don't have any other settings related to spectacular outside of

INSTALLED_APPS += ["rest_framework", "drf_spectacular"]

We do also have coreapi, openapi schema endpoints, using which our current clients operate. Hence, we added a new URL for spectacular schema. I hope coexistence of multiple schema apps is not an issue here.

abhinavsingh commented 2 years ago

So here is how we are generating our coreapi schema endpoints:

path(
        "schema/",
        get_schema_view(
            title="API",
            description="REST API",
            version="1.0.0",
            urlconf="api.backend.urls",
            generator_class=coreapi.SchemaGenerator,
        ),
        name="coreapi-schema",
    ),

Because of our app architecture, passing urlconf is necessary in most places. As we don't have a global urlconf for everything.

I figured, similar is certainly needed for spectacular, so I tried this:

path(
        "spectacular-schema/",
        SpectacularAPIView.as_view(urlconf="api.backend.urls"),
        name="spectacular-schema",
    ),

After the above change, querying the schema leads into the below MRO errors (note: I disabled all other schema endpoints for now)

.venv/lib/python3.9/site-packages/drf_spectacular/generators.py", line 163, in create_view
    action_schema_class = type('ExtendedRearrangedSchema', mro, {})
TypeError: duplicate base class AutoSchema

Unsure, if disabling other endpoints is even necessary. But at-least I got an error instead of empty schema :)

Further, I debugged the MRO values. Luckily, they do contain the APIs I am expecting to be discovered. But, it doesn't seem to handle them correctly:

(<class 'payments.backend.viewsets.order.OrderCreationSchema'>, <class 'rest_framework.schemas.coreapi.AutoSchema'>, <class 'payments.backend.viewsets.order.OrderSchema'>, <class 'rest_framework.schemas.coreapi.AutoSchema'>, <class 'rest_framework.schemas.inspectors.ViewInspector'>, <class 'object'>)

Futher, I checked upon the order viewsets. I see the author of this API has used multiple schema approach in their viewsets. i.e. they have a viewset level default schema and then they have an override at @action(schema=...) level within the same viewset for a custom path.

I think this use case is valid from the author of this API and should be allowed. Wdyt? But spectacular seems to be having trouble parsing such a schema. Looking at MRO above, likely it detected both OrderCreationSchema and OrderSchema and led into this problems. I am unsure if this is a valid pattern for openapi v3 though.

When I diged further, I realized, we have used from rest_framework.schemas.coreapi import AutoSchema everywhere, which was natural. Updating them to from drf_spectacular.openapi import AutoSchema resolved the MRO issues but then it broke our schema definition itself.

E.g. I see author of order API has used

class OrderSchema(AutoSchema):
    def get_filter_fields(self, path: str, method: str) -> List[Dict[str, Any]]:
        fields = super().get_filter_fields(path, method)

but now get_filter_fields is not valid and likely must be updated.

To summarize, we now have following insights:

  1. Empty schema was fixed once we started passing our custom urlconf
  2. This led to MRO issues, which are linked to using the wrong AutoSchema classes when defining custom schema's. Authors at our end must update these???

My initial expectation was DRF Spectacular will work out of box without requiring us to update AutoSchema class references. We already have "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema" set. Because, if we do have to update code to tie with Spectacular, this brings up another case altogether, as this will be equivalent to lock-in. We don't even know if Spectacular will work out for us.

Any workaround/suggestions? Anything that we aren't doing right over here.

Best

tfranzel commented 2 years ago

I hope coexistence of multiple schema apps is not an issue here.

yes that should not be an issue in principle, but changing DEFAULT_SCHEMA_CLASS to our schema might break core schema.

My initial expectation was DRF Spectacular will work out of box without requiring us to update AutoSchema class references.

spectacular only requires you to set DEFAULT_SCHEMA_CLASS as that contains the core functionality. everything else is up to you. You do need to use our class though. The readme clearly states this. However, you are free to subclass our AutoSchema and change things there. Your class then need to be set to DEFAULT_SCHEMA_CLASS

MRO errors

this looks like something broken with the schema class as this is heavily tested. your schema class needs to subclass from our schema. if you did that right, we might have a bug, but that is unlikely due to this working for a lot of people for a long time.

SpectacularAPIView.as_view(urlconf="api.backend.urls"),

setting urlconf like that is valid usage. should work.

SpectacularAPIView.as_view(api_version="v1"),

once you set api_version, the versioning_class in SpectacularAPIView does not matter anymore. You are overriding the estimated version. same goes for mount point for namespace versioning.

a schema is properly versioned when you see e.g. version: 0.0.0 (v1)

When I diged further, I realized, we have used from rest_framework.schemas.coreapi import AutoSchema everywhere, which was natural. Updating them to from drf_spectacular.openapi import AutoSchema resolved the MRO issues but then it broke our schema definition itself.

yes you cannot use the coreapi schema class. core api is deprecated and will be removed from DRF. the design is broken more or less.

I think this use case is valid from the author of this API and should be allowed. Wdyt?

the schema class is what makes spectacular work. it is simply impossible to change that. your developers approach cannot work here.

use our schema class and add filter params with @extend_schema to views. you can also modify the AutoSchema centrally, if those filters are recurring . You need to do this in fyi: we do support django-filter out of the box.

abhinavsingh commented 2 years ago

@tfranzel Thank you for advise on django-filter. I asked the API authors to remove usage of coreapi.AutoSchema and replace with django-filter. They did the change for some part of the code, and magically when I tried my spectacular branch today, I was presented with the Swagger UI, yay!!!

However, there are a couple of places where authors ran out of options:

  1. Non-filter fields: We have some APIs which doesn't map to any specific field in the model table. E.g. otp which is just the code sent by client. I see we are still using AutoSchema at some places for such use case. No wonder, such fields are not part of the schema yet. My understanding here is they must upgrade to using drf_spectacular.AutoSchema class for such use case. Is my understanding correct here?
  2. Auth: I am still patching SpectacularSwaggerView to inject our custom auth scheme. In our scheme there are 2-parts:

    1. Api Key
    2. Api Token, but unlike a hardcoded token/secret, we have a mechanism to generate this token per API request using the downloaded credentials.json by the clients. For access from the web, we have a mechanism where this token is specially generated to last for a few requests, after which, web SDK must grab a new token from the backend (usually automatically refreshed over WS channels).

    Since, token needs to be generated for every-other request, I had to customize SpectacularSwaggerView to inject necessary web tokens in the API requests. This seems to be working well. But I would like to remove this hack and work with OpenApiAuthenticationExtension. I created one, but looks like extensions can only define a single header value. One option would be to hack-up our 2-header requirements into 2 separate OpenApiAuthenticationExtension. Any suggestions? Though, I think, even after this, I'll need to hack it up still to inject our websocket client.

Just wanted to mention, drf_spectacular is really spectacular from my first experience. Thank you for your excellent work on it, cheers!!!

tfranzel commented 2 years ago

Thanks for the kind words.

  1. Subclassing the AutoSchema is usually a matter of last resort. I did quite a few complicated codebases and never needed to change the AutoSchema. You usually use the decorators first, most importantly @extend_schema(...) (example). The vast majority of issues can be resolved with that. For custom lib behavior, the extensions are usually the best fit. You can subclass AutoSchema if needed, but it is a lot less elegant and more error prone.

Please read up on https://drf-spectacular.readthedocs.io/en/latest/customization.html . Subclassing AutoSchema should only be needed after you exhausted all other steps.

  1. WS and custom token retrieval is outside the scope of spectacular. However, we do support multi parameter auth schemes via extension. This test should pretty much cover your use-case:

https://github.com/tfranzel/drf-spectacular/blob/c43b36476c00171b2c1d8dcb0e6fe05d6fe17c1f/tests/test_extensions.py#L128

abhinavsingh commented 2 years ago

@tfranzel Thank you for the multi_auth_scheme_extension example, didn't realize a list is also a valid return type. I have asked authors to migrate using @extend_schema decoration. I hope this will get us past the last hurdle.

During schema generation using command line and while making API requests, I observed following logs:

Warning #0: enum naming encountered a non-optimally resolvable collision for fields named "status". The same name has been used for multiple choice sets in multiple components. The collision was resolved with "Status823Enum". add an entry to ENUM_NAME_OVERRIDES to fix the naming.
Warning #1: encountered multiple names for the same choice set (ProviderSubscriptionStatusEnum). This may be unwanted even though the generated schema is technically correct. Add an entry to ENUM_NAME_OVERRIDES to fix the naming.

ENUM_NAME_OVERRIDES trick seems to solve this. Example I added

"ENUM_NAME_OVERRIDES": {
    "ProviderSubscriptionStatusEnum": "path.to.ProviderSubscriptionStatus",
},

But I am trying to understand what wrong has the API author done to run into ENUM_NAME_OVERRIDES requirements. I checked on ProviderSubscriptionStatus and it's being used only in a single model as choices=ProviderSubscriptionStatus.choices. Outside of model, this enum is used in views at various places for queryset.

Can you throw some light on why are we running into this issue. I'll prefer authors not to deal with spectacular settings at all :)

Best Abhinav

tfranzel commented 2 years ago

hey @abhinavsingh, glad to hear you are progressing. The issue with enum naming is a limitation of DRF, which we cannot circumvent. Your developers are doing nothing wrong, it is just that DRF is not providing enough information to make this work 100% of the time.

Your class ProviderSubscriptionStatus is not visible to DRF because you need to put in .choices directly. After everything is loaded that class is simply not associated with the serializer and thus we cannot get a name for it. All we have is the field name and the list of choices. We try to be smart where possible, but we cannot use what we do not have. If you inspect that serializer class you will notice that there is no connection to the ProviderSubscriptionStatus class anywhere. You could call that a design flaw of DRF.

You either need to do ENUM_NAME_OVERRIDES, ignore the warning, or disable the postprocessing hook by setting 'POSTPROCESSING_HOOKS: []'. There is no other solution. Removing the hook will leave all enums inlined, which may or may not work for you.

abhinavsingh commented 2 years ago

@tfranzel Thank you for your guidance while we were at it. Looks like team is all set. They migrated to @auto_schema where necessary and/or used django-filters. I agree with the enum problem being a side-effect of DRF limitation. I have conveyed the same to the team, they were blaming me for having to use VALUE_1 😂

Overall our team is happy with way it has turned out. We have been able to try python and typescript-fetch generators till now with success. Next we'll try out dart, swift, java generators.

About python, what is your recommendation for client generation:

I see they both have different structure for generated client module and how it gets used during integration.

I am curious to hear your experience with either of these and what fits best with spectacular.

Best

tfranzel commented 2 years ago

good to hear.

I have used python and typescript-fetch targets for openapi-generator. It has worked very well for me. spectacular explicitly makes a few concessions so that code generators have an easier time. I have not used openapi-python-client personally, but I know that quite a few spectacular users do use it successfully. However, openapi-generator is of priority for spectacular, because it is the most widely used code generator.

For users that generate code from the schema, I highly recommend using the setting 'COMPONENT_SPLIT_REQUEST': True,. It leads to significantly better model code with less read-only/write-only issues.

wtzr commented 1 year ago

I'm keep scratching my head over this one. I'm also getting empty schema without warnings. Possibly something with my path routing, but i can't figure out what it is. Somebody that can maybe spot the error? I'm not using any versioning in my views.

urls.py

from django.urls import include, path
from rest_framework import routers
from . import views
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView

router = routers.DefaultRouter()
router.register(r'transactions', views.TransactionViewSet)
router.register(r'transaction', views.TransactionReadOnlyViewSet)
router.register(r'tokens', views.TokenViewSet)

urlpatterns = [
    path('', include(router.urls)),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    path('schema/', SpectacularAPIView.as_view(), name='schema'),
    # Optional UI:
    path('schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
    path('schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),    

]

settings.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

SPECTACULAR_SETTINGS = {
    'TITLE': 'Your Project API',
    'DESCRIPTION': 'Your project description',
    'SERVE_INCLUDE_SCHEMA': True,
    # OTHER SETTINGS
}

Out put when running manage.py spectacular:

openapi: 3.0.3
info:
  title: Your Project API
  version: 0.0.0
  description: Your project description
paths: {}
components: {}

Update

Never mind I found the error. Since we are using django hosts, it's important that the ROOT_URLCONF setting is set to the correct urls.py file. In our case, it was the wrong one, so drf-spectacular was not able to read the api urls.

tfranzel commented 1 year ago

@wtzr, ahh that makes sense. I was about to say that your setup looks fine. I would assume a correct ROOT_URLCONF would always be necessary to even have a functioning API, though not sure how exactly django-hosts injects itself.

FYI: if you only wish to expose a subset of your API, you may set URLCONF on the schema view:

SpectacularAPIView.as_view(urlconf='some_partial_urlconf')
wtzr commented 1 year ago

@tfranzel You are correct. ROOT_URLCONF should always be set. We use djange hosts specifically to root some traffic to app.x.com, some to analytics.x.com etc. However, our rooturl was set to the wrong host, and I assume this was the place where spectacular was looking at.

Works like a charm, now onwards with the schema. Looking forward to play around with it. Very neatly integrated with DRF. Well done ;)

IbrokhimCybervize commented 8 months ago

Hello. I am using versioning in spectacular and having problem with namespace with unit test. reverse function is not working. This is my main urls

path("api/schema/", SpectacularAPIView.as_view(api_version='v1'), name="schema"),
path("api/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
path("api/schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
path("api/v1/accounts/", include("accounts.v1.urls", namespace="v1")),
path("api/v1/payments/", include("payments.v1.urls", namespace="v1")),

When run test reverse function is only working for accounts, it is not working for payments. When I change their place like this

path("api/schema/", SpectacularAPIView.as_view(api_version='v1'), name="schema"),
path("api/schema/swagger-ui/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"),
path("api/schema/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
path("api/v1/payments/", include("payments.v1.urls", namespace="v1")),
path("api/v1/accounts/", include("accounts.v1.urls", namespace="v1")),

Then reverse function for payments starts working, and for accounts it stops working.

How can resolve this problem