diff --git a/docs/faq.rst b/docs/faq.rst index faced16f..b80770a1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -82,3 +82,16 @@ You can easily specify a custom authentication with :py:class:`OpenApiAuthenticationExtension `. Have a look at :ref:`customization` on how to use ``Extensions`` + +FileField (ImageField) is not handled properly in the schema +------------------------------------------------------------ +In contrast to most other fields, ``FileField`` behaves differently for requests and responses. +This duality is impossible to represent in a single component schema. + +For these cases, there is an option to split components into request and response parts +by setting ``COMPONENT_SPLIT_REQUEST = True``. Note that this influences the whole schema, +not just components with ``FileFields``. + +Also consider explicitly setting ``parser_classes = [parsers.MultiPartParser]`` (or any file compatible parser) +on your `View` or write a custom `get_parser_classes`. These fields do not work with the default ``JsonParser`` +and that fact should be represented in the schema. \ No newline at end of file diff --git a/drf_spectacular/openapi.py b/drf_spectacular/openapi.py index 0d710d1f..d0f3dcd6 100644 --- a/drf_spectacular/openapi.py +++ b/drf_spectacular/openapi.py @@ -537,16 +537,12 @@ def _map_serializer_field(self, field, direction): return append_meta(content, meta) if isinstance(field, serializers.FileField): - if direction == 'response': - use_url = getattr(field, 'use_url', api_settings.UPLOADED_FILES_USE_URL) - if use_url: - return append_meta(build_basic_type(OpenApiTypes.URI), meta) - else: - return append_meta(build_basic_type(OpenApiTypes.STR), meta) + if spectacular_settings.COMPONENT_SPLIT_REQUEST and direction == 'request': + content = build_basic_type(OpenApiTypes.BINARY) else: - content = build_basic_type(OpenApiTypes.STR) - content['format'] = 'binary' - return append_meta(content, meta) + use_url = getattr(field, 'use_url', api_settings.UPLOADED_FILES_USE_URL) + content = build_basic_type(OpenApiTypes.URI if use_url else OpenApiTypes.STR) + return append_meta(content, meta) if isinstance(field, serializers.SerializerMethodField): method = getattr(field.parent, field.method_name) diff --git a/tests/__init__.py b/tests/__init__.py index 3b302021..63cc33af 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -39,9 +39,9 @@ def generate_schema(route, viewset=None, view=None, view_function=None): return schema -def get_response_schema(operation, status='200'): - return operation['responses'][status]['content']['application/json']['schema'] +def get_response_schema(operation, status='200', content_type='application/json'): + return operation['responses'][status]['content'][content_type]['schema'] -def get_request_schema(operation): - return operation['requestBody']['content']['application/json']['schema'] +def get_request_schema(operation, content_type='application/json'): + return operation['requestBody']['content'][content_type]['schema'] diff --git a/tests/test_regressions.py b/tests/test_regressions.py index e2265c60..14aeb056 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -5,7 +5,7 @@ from django.db.models import fields from django.db import models from django.urls import path, re_path -from rest_framework import serializers, viewsets, mixins, routers, views, generics +from rest_framework import serializers, viewsets, mixins, routers, views, generics, parsers from rest_framework.decorators import action, api_view from rest_framework.views import APIView @@ -569,3 +569,24 @@ class XView(generics.ListAPIView): operation = schema['paths']['/x']['get'] assert operation['operationId'] == 'x_list' assert get_response_schema(operation)['type'] == 'array' + + +@mock.patch('drf_spectacular.settings.spectacular_settings.COMPONENT_SPLIT_REQUEST', True) +def test_file_field_duality_on_split_request(no_warnings): + class XSerializer(serializers.Serializer): + file = serializers.FileField() + + class XView(generics.ListCreateAPIView): + serializer_class = XSerializer + parser_classes = [parsers.MultiPartParser] + + schema = generate_schema('/x', view=XView) + assert get_response_schema( + schema['paths']['/x']['get'] + )['items']['$ref'] == '#/components/schemas/X' + assert get_request_schema( + schema['paths']['/x']['post'], content_type='multipart/form-data' + )['$ref'] == '#/components/schemas/XRequest' + + assert schema['components']['schemas']['X']['properties']['file']['format'] == 'uri' + assert schema['components']['schemas']['XRequest']['properties']['file']['format'] == 'binary'