drf-spectacular: Auto schema generation doesn't support pydantic schema

Describe the bug I currently use SQLModel and Pydantic to support queries to my database and serialization of said data into json. Pydantic comes with a handy schema feature for all BaseModels that generates either a json or dict schema.

When I use a single model with no nested fields and pass in the Model’s dict schema to the @extend_schema decorator as one o the response params, ex: responses={200: my_model.schema()}, this works fine. However, if I have a model with another model as one of its fields (see here for an example), the schema generated from that isn’t interpreted correctly by drf-spectacular.

Specifically, the problem seems to be that Pydantic generates its documentation for nested models inside of a definitions key. I’m wondering if support could be added to drf-spectacular to move anything declared under the definitions keyword to the components/schema section of the api docs. Pydantic does add support for customizing the $ref location/prefix for models referenced elsewhere, so this wouldn’t be an issue.

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 29 (29 by maintainers)

Commits related to this issue

Most upvoted comments

@tfranzel Excellent! With the new master version, it seems to work well!

I did these changes in my project to generate doc correctly:

Copied the master version of the extension to myapp/schema.py.

diff --git a/myapp/api/definitions/v1/myview.py b/myapp/api/definitions/v1/myview.py
index df4ca32..652df66 100644
--- a/myapp/api/definitions/v1/myview.py
+++ b/myapp/api/definitions/v1/myview.py
@@ -23,7 +23,7 @@ def get_list_my_view_schema() -> dict:
                 location=OpenApiParameter.PATH,
             ),
         ],
-        responses={200: OpenApiResponse(response=MyModel.schema(), description="Some description")},
+        responses={200: OpenApiResponse(response=MyModel, description="Some description")},
     )   
 
     return {"get": list_schema}
diff --git a/myapp/apps.py b/myapp/apps.py
index 81898f5..432abfb 100644
--- a/myapp/apps.py
+++ b/myapp/apps.py
@@ -20,3 +20,4 @@ class MyAppConfig(AppConfig):
     def ready(self):
 
+        import myapp.schema
diff --git a/myapp/settings.py b/myapp/settings.py
index f8de196..4dde1bb 100644
--- a/myapp/settings.py
+++ b/myapp/settings.py
@@ -264,8 +264,4 @@ SPECTACULAR_SETTINGS = {
     "SCHEMA_PATH_PREFIX": "/v[1-2]",
     "SERVE_INCLUDE_SCHEMA": False,
-    "POSTPROCESSING_HOOKS": [
-        "drf_spectacular.hooks.postprocess_schema_enums",
-        "myapp.common.openapi.postprocessing",
-    ],
 }

Thanks!

Indeed, I’m using it in a list, but in a ListAPIView, not in a ViewSet.

I see, they are very similar and we are dealing with them almost interchangeably.

At least we got it working in principle. Those minors are the reason we have no official support. The fact that the pydantic models are not based on serializers is kind of a monkey wrench in the gears.

cheers!

@caarmen, beware that you might want to add the (default on) enum processing back into the mix:

"POSTPROCESSING_HOOKS": [
    "drf_spectacular.hooks.postprocess_schema_enums", "com.myexample.postprocessing", 
],

I will try the serializer extension route for completeness when I have some spare time and report results here.

I moved my models into an isolated module. I can now use the APPEND_COMPONENTS as mentioned in https://github.com/tfranzel/drf-spectacular/issues/1006#issuecomment-1597273154 👍🏻

However, using the extension instead of APPEND_COMPONENTS results in the original behavior raised in this issue.

please print a debug message in the ready() method to make sure the extension is actually imported. … Also do a print in myapp.schema on the bottom of the file just to be sure.

$ python -m manage spectacular --format openapi-json --file "/tmp/openapi.json"
ready
bottom of schema module

I’ll see if I can debug this and find anything further.

I added prints to get_name() and map_serializer() and they don’t appear.

awesome @sydney-runkle, glad this works now. 🎉

@caarmen I think once you fix your app loading it should work for you too.

I attempted to do the serializer extension. Turns out Pydantic has some specifics we do not yet account for. So in the spirit of “eat your own dog food”, I fixed 2 minors to allow for proper functionality of this extension:

https://github.com/tfranzel/drf-spectacular/pull/1009/files#diff-5f1057f5cbe9827ecd29bac35d534f40e4beb52a49f53206bae7ad82bc4f2e4d

I added the extension to the blueprint section. Not sure yet whether we should support this officially, but people can at least copy&paste that extension for the time being.

Feedback would be much appreciated because I hastily put this together.

@sydney-runkle Thanks!

I think my issues are that my pydantic model classes are defined in a module that tries to access some drf code that requires the app being initialized. I assume I could refactor this and move those classes to an isolated module. I didn’t try it yet though, but I’ll be sure to come back here and check out this snippet if I do get a chance. Thanks again 😃

Sure thing! Here’s what I used for the APPEND_COMMENTS route:

# views.py
from drf_spectacular.utils import extend_schema

@extend_schema(
        parameters=my_params,
        responses={
            200: {"$ref": "#/components/schemas/Model"}
        }
    )
def get(...)
    ...
# settings.py
from pydantic.schema import schema

SPECTACULAR_SETTINGS = {
    ...
    'APPEND_COMPONENTS': {
        "schemas": schema(
            [Model], 
            by_alias=True, 
            ref_template="#/components/schemas/{model}"
        )["definitions"]
    }
}

I’ve got tons of pydantic models I need to include the schema for in the openapi docs, so I plan to create a module that abstracts away the pydantic.schema(...) logic so that I can keep the settings.py file clean.

Let me know if you have any additional questions! Nice to work with you all on this.

Funny there are a couple of us with the same need on the same day 😅

I tried the postprocessing hook and it seems to work 💪🏻

This example is explicitly handling specific models, it’s not generic.

Pydantic models (from link above):

class Foo(BaseModel):
    a: int


class Model(BaseModel):
    a: Foo

View: Define a response of the outer model type Model:

class MyView(generics.ListAPIView):


    @extend_schema(
        responses={
            200: OpenApiResponse(response=Model.schema())
        }
    )
    def get(self, request, *args, **kwargs):

post processing function in com.myexample module: Add the inner model Foo to the definitions:

def postprocessing(result, generator, request, public):
    definitions = result.setdefault("definitions", {})
    definitions["Foo"] = Foo.schema()
    return result

settings: Declare the post processing hook

    "POSTPROCESSING_HOOKS": ["com.myexample.postprocessing"],

https://github.com/tfranzel/drf-spectacular/blob/master/drf_spectacular/hooks.py

the second one is pretty basic. please read the above doc section carefully.

I think it might also be possible to write a serializer extension to accommodate this. I’ll throw a quick test together later but will most certainly not include it officially, which is okay as you can write/use extensions locally without issue.

I see. We accounted for raw schemas on a best effort basis but never really paid attention to this particular case.

I see 2 options (minus a big refactoring, which I am not that keen on):

  1. write a postprocessing hook that cleans up, i.e. removing the components from the path and adding them in the components section and inserting a ref instead. https://drf-spectacular.readthedocs.io/en/latest/customization.html#step-6-postprocessing-hooks

  2. only decorate a raw schema with a reference: {"$ref": "#/components/schemas/XXX"} and either use a postprocessing hook or the setting APPEND_COMPONENTS to fill in the definition part there with my_model.schema().

I think you can accomplish this with 1-2 helper functions and a bit of tooling. Keep in mind that OpenAPI ❤️.1 and JSONSchema are not 100% compatible. Usually not an issue, but your mileage may vary