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

How to use pydantic model in @extend_schema with "many=True" #1232

Open microHoffman opened 4 months ago

microHoffman commented 4 months ago

Hello! I have pydantic v2 model and I'd like to annotate my view using @extend_schema(response=...) that it returns the list of this pydantic model instances, something similar as you can do with DRF serializer using many=True... however I did not find a way how I can make this work correctly... Can you please help me what is a way of doing so? I've tried TypeAdapter, List[MyModel] and some other combinations, but neither of them seems to return correct openapi schema with correctly defined refs in #/components/schemas/{model}... Thanks a lot!

ignis-tech-solutions commented 4 months ago

Did you find any solution for this @microHoffman?

cc: @tfranzel if you can help please

ignis-tech-solutions commented 3 months ago

Found a workaround for it, we can declare a new model using the pydantic.RootModel:

class MyModel(RootModel):
    root: List[MyModel]

and use it with extend_schema as:

@extend_schema(
        request=OpenApiRequest(request=MyModels),
        ...
)
microHoffman commented 3 months ago

Found a workaround for it, we can declare a new model using the pydantic.RootModel:

class MyModel(RootModel):
    root: List[MyModel]

and use it with extend_schema as:

@extend_schema(
        request=OpenApiRequest(request=MyModels),
        ...
)

Thanks a lot for sharing your solution!:)

tfranzel commented 3 months ago

sry for not replying earlier. I actually looked into this @microHoffman, but was unsure whether we can anything about it.

Basically there is some functionality gap between real serializers and the pydantic plugin. Since list handling is kind of special in DRF, there is not really a easy way of integrating List[MyModel] without some hefty refactoring. For TypeAdapter I hit some other issue I was not able to solve with the amount of time I had.

I'm glad that the RootModel this is a viable workaround even if it is not the prettiest. thx @ignis-tech-solutions

@ignis-tech-solutions the key part is the RootModel here. The OpenApiRequest wrapping should not be necessary.

ghabbenjansen commented 6 days ago

For anyone running into the same problem, we have managed to add this functionality by using a wrapper class together with a custom extension.

The idea behind this solution is:

class DataclassList:
    def __init__(self, model: type[BaseModel]):
        self.model = model

class PydanticListExtension(OpenApiSerializerExtension):
    target_class = "path.to.DataclassList"

    def get_name(self, auto_schema, direction) -> str:
        return f"{self.target.model.__name__}List"

    def map_serializer(self, auto_schema: AutoSchema, direction: Direction) -> _SchemaType:
        # Register schema for component
        schema = model_json_schema(self.target.model, ref_template="#/components/schemas/{model}", mode="serialization")

        # pull out potential sub-schemas and put them into component section
        for sub_name, sub_schema in schema.pop("$defs", {}).items():
            component = ResolvedComponent(
                name=sub_name,
                type=ResolvedComponent.SCHEMA,
                object=sub_name,
                schema=sub_schema,
            )
            auto_schema.registry.register_on_missing(component)

        # Register the target component directly
        component = ResolvedComponent(
            name=self.target.model.__name__,
            type=ResolvedComponent.SCHEMA,
            object=self.target.model.__name__,
            schema=schema
        )
        auto_schema.registry.register_on_missing(component)

        result = {
            "type": "array",
            "items": auto_schema.registry[component.key].ref
        }

        return result

Note: Fix the import path at the target_class field in the PydanticListExtension.

This wrapper is used as follows:

@extend_schema(request=DataclassList(MyDataClass))
def some_action(self, request, *args, **kwargs):
    ...    

@tfranzel Do you think this is worthy adding to the package (perhaps as a blueprint), given it is support for a very specific use-case? If so, I will be be hapy to open a PR integrating and testing these changes.