diff --git a/docs/blueprints.rst b/docs/blueprints.rst index 85777890..e6d61fb7 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -146,4 +146,13 @@ django-parler-rest `django-parler-rest `_ integration for translation package `django-parler `_. -.. literalinclude:: blueprints/django_parler_rest.py \ No newline at end of file +.. literalinclude:: blueprints/django_parler_rest.py + + +Pydantic +-------- + +Preliminary support for `Pydantic `_ models. This may or may not +end up in the main package. Catches decorated Pydantic classes and integrates their schema. + +.. literalinclude:: blueprints/pydantic.py \ No newline at end of file diff --git a/docs/blueprints/pydantic.py b/docs/blueprints/pydantic.py new file mode 100644 index 00000000..aff62fa8 --- /dev/null +++ b/docs/blueprints/pydantic.py @@ -0,0 +1,36 @@ +from drf_spectacular.extensions import OpenApiSerializerExtension +from drf_spectacular.plumbing import ResolvedComponent + + +class PydanticExtension(OpenApiSerializerExtension): + target_class = 'pydantic.BaseModel' + match_subclasses = True + + def get_name(self, auto_schema, direction): + return self.target.__name__ + + def map_serializer(self, auto_schema, direction): + def translate_refs(obj, key=None): + if isinstance(obj, dict): + return {k: translate_refs(v, k) for k, v in obj.items()} + elif isinstance(obj, list): + return [translate_refs(i) for i in obj] + elif key == '$ref': + return obj.replace('#/definitions/', '#/components/schemas/') + else: + return obj + + # let pydantic generate a JSON schema + schema = self.target.schema() + + # pull out potential sub-schemas and put them into component section + for sub_name, sub_schema in schema.pop('definitions', {}).items(): + component = ResolvedComponent( + name=sub_name, + type=ResolvedComponent.SCHEMA, + object=sub_name, + schema=translate_refs(sub_schema), + ) + auto_schema.registry.register_on_missing(component) + + return translate_refs(schema) diff --git a/drf_spectacular/plumbing.py b/drf_spectacular/plumbing.py index 8b659662..6fe912dc 100644 --- a/drf_spectacular/plumbing.py +++ b/drf_spectacular/plumbing.py @@ -146,8 +146,8 @@ def is_basic_type(obj, allow_none=True): def is_patched_serializer(serializer, direction): return bool( spectacular_settings.COMPONENT_SPLIT_PATCH - and serializer.partial - and not serializer.read_only + and getattr(serializer, 'partial', None) + and not getattr(serializer, 'read_only', None) and not (spectacular_settings.COMPONENT_SPLIT_REQUEST and direction == 'response') ) @@ -764,7 +764,11 @@ def _matches(cls, target) -> bool: if cls.target_class is None: return False # app not installed elif cls.match_subclasses: - return issubclass(get_class(target), cls.target_class) # type: ignore + # Targets may trigger customized check through __subclasscheck__. Attempt to be more robust + try: + return issubclass(get_class(target), cls.target_class) # type: ignore + except TypeError: + return False else: return get_class(target) == cls.target_class