Hipo/drf-extra-fields

Unable to make the ImageField as required in Swagger POST API

MounikaKommineni opened this issue ยท 29 comments

My model:

class Image(models.Model):
      image=models.ImageField(upload_to='photos')
      name=models.CharField(max_length=40,unique=False)

My serializer:

class imagesSerializer(serializers.ModelSerializer):
      image = Base64ImageField(required=True)
      class Meta:
         model = Image
         fields = ('id','name','image')

My swagger view:
kfrof

How to make the imagefield as required in swagger post request ?

I noticed this too in generated Angular TypeScript. The image variables were always marked with ?, meaning they were optional.

I am also facing the same issue even though i specified required=True in serializer class

I'm using the same serializer setup and mine seems to be working fine. Maybe this is an issue of differences in versions. Here are the versions of Django, Django Extra Fields, Django REST Framework and Django REST Swagger that I have used, so you can compare: Django==2.1.7, django-extra-fields==1.0.0, djangorestframework==3.9.2, django-rest-swagger==2.2.0.

swagger_correct

I wonder if it's because of the readOnly property? Would a readOnly property make it optional because it's not required when writing to the API? Is this because of how the file is normally returned as a URL rather than the data directly? (I'm just spit balling at this point).

ReadOnly fields cannot be required at the same time, therefore it's not possible to have a field where both required and read_only are set to True in the latest version of Django REST Framework.

What's weird is in my case, I never set read_only to True. The serializer is a WritableNestedSerializer, but I don't see how that would affect the field.

I've used the serializer below:

class ImageSerializer(WritableNestedModelSerializer):
    image = Base64ImageField(required=True)

    class Meta:
        model = Image
        fields = ('id', 'name', 'image')

It still seems to be working fine.

swagger

@baranugur Could you also post your Image Model?

I wonder why your image field doesn't say string($uri). That is how mine always is (and the OP's).

Also, I'm using drf-yasg == 1.13.0 instead of django-rest-swagger due to django-rest-swagger having low maintenance as of late. Perhaps others in the thread could comment on which they are using.

The model that I've created is identical to OP's.

I think drf-yasg==1.13.0 is causing the problem here. I've just tried it with Base64ImageField and then I've used the built-in serializers.ImageField; this is what I've gotten in both cases:

yasg

Confirmed this is drf-yasg specific (intended) behavior:

axnsan12/drf-yasg#332 (comment)

That is indeed the case. The field you linked breaks the contract of FileField by making it accept a string instead of an UploadedFile.

You can work around this with a custom FieldInspector that returns an appropriate Schema object of type string, which should be placed before the FileFieldInspector.

Marking as completed.

@yigitguler There may be a way to work around this (at least in documentation): axnsan12/drf-yasg#332 (comment)

I don't quite understand what needs to be done, but if this issue were left open it might be useful. We may be able to at least document how to make this work with drf-yasg, which I think would be valuable given it's the best maintained OpenAPI generator available for Django currently.

Do you happen to understand the comment in the link above, and how we could fix it?

We can reopen the issue. However, I think there is nothing we can do to fix this situation. Developers of drf-yasg assumed that only multipart requests can contain files. Which is not true for our use case.

Using Custom Schema Generation may be a method to solve this.

EDITED : Fix the class checking algorithm. My previous answer does not work on FileField or FieldMixin.

Hi! This is the work around code that I've overridden FieldInspector to mark Base64ImageField, Base64FileField, Base64FieldMixin as required field
NOTE: I've to remove format from the String because the POST, PUT, PATCH... method use SchemaRef which reference the schema from GET method.

from drf_yasg import openapi
from drf_yasg.inspectors import FieldInspector, SwaggerAutoSchema
from drf_yasg.app_settings import swagger_settings


class Base64FileFieldInspector(FieldInspector):
    BASE_64_FIELDS = ['Base64ImageField', 'Base64FileField', 'Base64FieldMixin']

    def __classlookup(self, cls):
        """List all base class of the given class"""
        c = list(cls.__bases__)
        for base in c:
            c.extend(self.__classlookup(base))
        return c

    def process_result(self, result, method_name, obj, **kwargs):
        if isinstance(result, openapi.Schema.OR_REF):
            base_classes = [x.__name__ for x in self.__classlookup(obj.__class__)]
            if any(item in Base64FileFieldInspector.BASE_64_FIELDS for item in base_classes):
                schema = openapi.resolve_ref(result, self.components)
                schema.pop('readOnly', None)
                schema.pop('format', None)  # Remove $url format from string

        return result


class Base64FileAutoSchema(SwaggerAutoSchema):
    field_inspectors = [Base64FileFieldInspector] + swagger_settings.DEFAULT_FIELD_INSPECTORS


class FormAttachmentViewSet(viewsets.ModelViewSet):
    queryset = .......
    serializer_class = ..........
    swagger_schema = Base64FileAutoSchema

Result:
Screen Shot 2562-08-21 at 21 15 39

@WasinTh Looks pretty neat -- would you mind opening a PR to this repo's README? I think it would be good to document this for others and for others to test out.

@johnthagen sure this is the pull request on README.md file
#100

How about make it some example code to testprojet, this repo example does not have about flieupload or imageupload,it would be make good to use

In Swagger API, I'm using the following setup to show the field as required without any problems:

class PDFBase64FileField(Base64FileField):
    ALLOWED_TYPES = ['pdf']

    class Meta:
        swagger_schema_fields = {
            'type': 'string',
            'title': 'File Content',
            'description': 'Content of the file base64 encoded',
            'read_only': False  # <-- FIX
        }

    def get_file_extension(self, filename, decoded_file):
        try:
            PyPDF2.PdfFileReader(io.BytesIO(decoded_file))
        except PyPDF2.utils.PdfReadError as e:
            logger.warning(e)
        else:
            return 'pdf'

And to use "string" format instead of an URL, you will need to create the field with use_url=False.

@ilmesi Thank you for the suggestion. This seems pretty simple to me compered to creating a FieldInspector. What do you think @WasinTh ?

@alicertel No problem! I got it from the code at FieldInspector.add_manual_fields from yasg. It seems to work pretty well and it's less work than creating a new FieldInspector..

I can confirm that @ilmesi's answer works. It seems like the most ideal work-around thus far.

It would be good if this was documented somewhere officially.

@ilmesi Thank you for the suggestion. This seems pretty simple to me compered to creating a FieldInspector. What do you think @WasinTh ?

Agreed. The solution to override Base64FileField is much more simple.

@WasinTh Would you like to update your PR #100 with the suggested solution #66 (comment) ?

@WasinTh Would you like to update your PR #100 with the suggested solution #66 (comment) ?

@alicertel Sure. I've pushed the updated answer on those PRs.

you should change your parser like this :

    from rest_framework import permissions, viewsets
    from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
                                       ListModelMixin, RetrieveModelMixin)
    from rest_framework.parsers import FormParser, MultiPartParser
    from .models import Customer
    from .permissions import CustomerPermission
    from .serializer import CustomerSerializer
    
    
    class CustomerViewSet(CreateModelMixin, ListModelMixin, RetrieveModelMixin, 
                          DestroyModelMixin, viewsets.GenericViewSet):
        permission_classes = [CustomerPermission]
        queryset = Customer.objects.all()
        serializer_class = CustomerSerializer
        parser_classes = (FormParser, MultiPartParser)

after that you can check your swagger :

image

I hope this can help you

If you want create no class views but use drf:

register_parameters = [openapi.Parameter(
    'date_of_birth', in_=openapi.IN_FORM, description='Add date like 2000-12-12', type=openapi.FORMAT_DATE,
    required=True)]


@swagger_auto_schema(methods=['post'], request_body=RegisterSerializer, manual_parameters=register_parameters)
@ api_view(['POST', ])
@ parser_classes([parsers.MultiPartParser, parsers.FormParser])
def user_register_serializer(request):
    if request.method == 'POST':
        new_data = {
            'response': f"Email send to {request.data['email']}",
        }
        serializer = RegisterSerializer(data=request.data)
        if serializer.is_valid():
            new_data.update(serializer.data)
            new_data.pop('password')
            serializer.save()
            user_email_send(request)
        else:
            new_data = serializer.errors
        return Response(new_data)

using manual_parameters you can override form in swagger and add new functions like default etc to image.