Django Rest Framework File Upload

Viewed 215098

I am using Django Rest Framework and AngularJs to upload a file. My view file looks like this:

class ProductList(APIView):
    authentication_classes = (authentication.TokenAuthentication,)
    def get(self,request):
        if request.user.is_authenticated(): 
            userCompanyId = request.user.get_profile().companyId
            products = Product.objects.filter(company = userCompanyId)
            serializer = ProductSerializer(products,many=True)
            return Response(serializer.data)

    def post(self,request):
        serializer = ProductSerializer(data=request.DATA, files=request.FILES)
        if serializer.is_valid():
            serializer.save()
            return Response(data=request.DATA)

As the last line of post method should return all the data, I have several questions:

  • how to check if there is anything in request.FILES?
  • how to serialize file field?
  • how should I use parser?
22 Answers

From my experience, you don't need to do anything particular about file fields, you just tell it to make use of the file field:

from rest_framework import routers, serializers, viewsets

class Photo(django.db.models.Model):
    file = django.db.models.ImageField()

    def __str__(self):
        return self.file.name

class PhotoSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.Photo
        fields = ('id', 'file')   # <-- HERE

class PhotoViewSet(viewsets.ModelViewSet):
    queryset = models.Photo.objects.all()
    serializer_class = PhotoSerializer

router = routers.DefaultRouter()
router.register(r'photos', PhotoViewSet)

api_urlpatterns = ([
    url('', include(router.urls)),
], 'api')
urlpatterns += [
    url(r'^api/', include(api_urlpatterns)),
]

and you're ready to upload files:

curl -sS http://example.com/api/photos/ -F 'file=@/path/to/file'

Add -F field=value for each extra field your model has. And don't forget to add authentication.

After spending 1 day on this, I figured out that ...

For someone who needs to upload a file and send some data, there is no straight fwd way you can get it to work. There is an open issue in json api specs for this. One possibility i have seen is to use multipart/related as shown here, but i think its very hard to implement it in drf.

Finally what i had implemented was to send the request as formdata. You would send each file as file and all other data as text. Now for sending the data as text you have two choices. case 1) you can send each data as key value pair or case 2) you can have a single key called data and send the whole json as string in value.

The first method would work out of the box if you have simple fields, but will be a issue if you have nested serializes. The multipart parser wont be able to parse the nested fields.

Below i am providing the implementation for both the cases

Models.py

class Posts(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
    caption = models.TextField(max_length=1000)
    media = models.ImageField(blank=True, default="", upload_to="posts/")
    tags = models.ManyToManyField('Tags', related_name='posts')

serializers.py -> no special changes needed, not showing my serializer here as its too lengthy because of the writable ManyToMany Field implimentation.

views.py

class PostsViewset(viewsets.ModelViewSet):
    serializer_class = PostsSerializer
    #parser_classes = (MultipartJsonParser, parsers.JSONParser) use this if you have simple key value pair as data with no nested serializers
    #parser_classes = (parsers.MultipartParser, parsers.JSONParser) use this if you want to parse json in the key value pair data sent
    queryset = Posts.objects.all()
    lookup_field = 'id'

Now, if you are following the first method and is only sending non-Json data as key value pairs, you don't need a custom parser class. DRF'd MultipartParser will do the job. But for the second case or if you have nested serializers (like i have shown) you will need custom parser as shown below.

utils.py

from django.http import QueryDict
import json
from rest_framework import parsers

class MultipartJsonParser(parsers.MultiPartParser):

    def parse(self, stream, media_type=None, parser_context=None):
        result = super().parse(
            stream,
            media_type=media_type,
            parser_context=parser_context
        )
        data = {}

        # for case1 with nested serializers
        # parse each field with json
        for key, value in result.data.items():
            if type(value) != str:
                data[key] = value
                continue
            if '{' in value or "[" in value:
                try:
                    data[key] = json.loads(value)
                except ValueError:
                    data[key] = value
            else:
                data[key] = value

        # for case 2
        # find the data field and parse it
        data = json.loads(result.data["data"])

        qdict = QueryDict('', mutable=True)
        qdict.update(data)
        return parsers.DataAndFiles(qdict, result.files)

This serializer would basically parse any json content in the values.

The request example in post man for both cases: case 1 case 1,

Case 2 case2

If anyone interested in the easiest example with ModelViewset for Django Rest Framework.

The Model is,

class MyModel(models.Model):
    name = models.CharField(db_column='name', max_length=200, blank=False, null=False, unique=True)
    imageUrl = models.FileField(db_column='image_url', blank=True, null=True, upload_to='images/')

    class Meta:
        managed = True
        db_table = 'MyModel'

The Serializer,

class MyModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel
        fields = "__all__"

And the View is,

class MyModelView(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer

Test in Postman,

enter image description here

models.py

from django.db import models

import uuid

class File(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    file = models.FileField(blank=False, null=False)
    
    def __str__(self):
        return self.file.name

serializers.py

from rest_framework import serializers
from .models import File

class FileSerializer(serializers.ModelSerializer):
    class Meta:
        model = File
        fields = "__all__"

views.py

from django.shortcuts import render
from rest_framework.parsers import FileUploadParser
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status

from .serializers import FileSerializer


class FileUploadView(APIView):
    permission_classes = []
    parser_class = (FileUploadParser,)

    def post(self, request, *args, **kwargs):

      file_serializer = FileSerializer(data=request.data)

      if file_serializer.is_valid():
          file_serializer.save()
          return Response(file_serializer.data, status=status.HTTP_201_CREATED)
      else:
          return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST)

urls.py

from apps.files import views as FileViews

urlpatterns = [
    path('api/files', FileViews.FileUploadView.as_view()),
]

settings.py

# file uload parameters
MEDIA_URL =  '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

Send a post request to api/files with a your file attached to a form-data field file. The file will be uploaded to /media folder and a db record will be added with id and file name.

I'd like to write another option that I feel is cleaner and easier to maintain. We'll be using the defaultRouter to add CRUD urls for our viewset and we'll add one more fixed url specifying the uploader view within the same viewset.

**** views.py 

from rest_framework import viewsets, serializers
from rest_framework.decorators import action, parser_classes
from rest_framework.parsers import JSONParser, MultiPartParser
from rest_framework.response import Response
from rest_framework_csv.parsers import CSVParser
from posts.models import Post
from posts.serializers import PostSerializer     


class PostsViewSet(viewsets.ModelViewSet):

    queryset = Post.objects.all()
    serializer_class = PostSerializer 
    parser_classes = (JSONParser, MultiPartParser, CSVParser)


    @action(detail=False, methods=['put'], name='Uploader View', parser_classes=[CSVParser],)
    def uploader(self, request, filename, format=None):
        # Parsed data will be returned within the request object by accessing 'data' attr  
        _data = request.data

        return Response(status=204)

Project's main urls.py

**** urls.py 

from rest_framework import routers
from posts.views import PostsViewSet


router = routers.DefaultRouter()
router.register(r'posts', PostsViewSet)

urlpatterns = [
    url(r'^posts/uploader/(?P<filename>[^/]+)$', PostsViewSet.as_view({'put': 'uploader'}), name='posts_uploader')
    url(r'^', include(router.urls), name='root-api'),
    url('admin/', admin.site.urls),
]

.- README.

The magic happens when we add @action decorator to our class method 'uploader'. By specifying "methods=['put']" argument, we are only allowing PUT requests; perfect for file uploading.

I also added the argument "parser_classes" to show you can select the parser that will parse your content. I added CSVParser from the rest_framework_csv package, to demonstrate how we can accept only certain type of files if this functionality is required, in my case I'm only accepting "Content-Type: text/csv". Note: If you're adding custom Parsers, you'll need to specify them in parsers_classes in the ViewSet due the request will compare the allowed media_type with main (class) parsers before accessing the uploader method parsers.

Now we need to tell Django how to go to this method and where can be implemented in our urls. That's when we add the fixed url (Simple purposes). This Url will take a "filename" argument that will be passed in the method later on. We need to pass this method "uploader", specifying the http protocol ('PUT') in a list to the PostsViewSet.as_view method.

When we land in the following url

 http://example.com/posts/uploader/ 

it will expect a PUT request with headers specifying "Content-Type" and Content-Disposition: attachment; filename="something.csv".

curl -v -u user:pass http://example.com/posts/uploader/ --upload-file ./something.csv --header "Content-type:text/csv"

Some of the solutions are deprecated (request.data should be used for Django 3.0+). Some of them do not validate the input. Also, I would appreciate a solution with swagger annotation. So I recommend using the following code:

from drf_yasg.utils import swagger_auto_schema
from rest_framework import serializers
from rest_framework.parsers import MultiPartParser
from rest_framework.response import Response
from rest_framework.views import APIView


class FileUploadAPI(APIView):
    parser_classes = (MultiPartParser, )

    class InputSerializer(serializers.Serializer):
        image = serializers.ImageField()

    @swagger_auto_schema(
        request_body=InputSerializer
    )
    def put(self, request):
        input_serializer = self.InputSerializer(data=request.data)
        input_serializer.is_valid(raise_exception=True)

        # process file
        file = input_serializer.validated_data['image']

        return Response(status=204)

    from rest_framework import status
    from rest_framework.response import Response
    class FileUpload(APIView):
         def put(request):
             try:
                file = request.FILES['filename']
                #now upload to s3 bucket or your media file
             except Exception as e:
                   print e
                   return Response(status, 
                           status.HTTP_500_INTERNAL_SERVER_ERROR)
             return Response(status, status.HTTP_200_OK)

If you are using ModelViewSet, well actually you are done! It handles every things for you! You just need to put the field in your ModelSerializer and set content-type=multipart/form-data; in your client.

BUT as you know you can not send files in json format. (when content-type is set to application/json in your client). Unless you use Base64 format.

So you have two choices:

  • let ModelViewSet and ModelSerializer handle the job and send the request using content-type=multipart/form-data;
  • set the field in ModelSerializer as Base64ImageField (or) Base64FileField and tell your client to encode the file to Base64 and set the content-type=application/json

I have used this view to upload file to aws. Here upload_file is a helper function while overall you can use this view to get upload the file in form-data.

class FileUploadView(GenericAPIView):

    def post(self, request):
        try:
            file = request.data['file']
            if file.content_type == 'image/png' or file.content_type == 'image/jpeg':
                file_name = upload_file(file)
                return Response({"name": file_name}, status=status.HTTP_202_ACCEPTED)
            else:
                raise UnsupportedMediaType(file.content_type)
        except KeyError:
            return Response("file missing.", status=status.HTTP_404_NOT_FOUND)
def post(self,request):
        serializer = ProductSerializer(data=request.DATA, files=request.FILES)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)

This is the one of the approach I've applied hopefully it'll help.

     class Model_File_update(APIView):
         parser_classes = (MultiPartParser, FormParser)
         permission_classes = [IsAuthenticated]  # it will check if the user is authenticated or not
         authentication_classes = [JSONWebTokenAuthentication]  # it will authenticate the person by JSON web token

         def put(self, request):
            id = request.GET.get('id')
            obj = Model.objects.get(id=id)
            serializer = Model_Upload_Serializer(obj, data=request.data)
            if serializer.is_valid():
               serializer.save()
               return Response(serializer.data, status=200)
            else:
               return Response(serializer.errors, status=400)

You can generalize @Nithin's answer to work directly with DRF's existing serializer system by generating a parser class to parse specific fields which are then fed directly into the standard DRF serializers:

from django.http import QueryDict
import json
from rest_framework import parsers


def gen_MultipartJsonParser(json_fields):
    class MultipartJsonParser(parsers.MultiPartParser):

        def parse(self, stream, media_type=None, parser_context=None):
            result = super().parse(
                stream,
                media_type=media_type,
                parser_context=parser_context
            )
            data = {}
            # find the data field and parse it
            qdict = QueryDict('', mutable=True)
            for json_field in json_fields:
                json_data = result.data.get(json_field, None)
                if not json_data:
                    continue
                data = json.loads(json_data)
                if type(data) == list:
                    for d in data:
                        qdict.update({json_field: d})
                else:
                    qdict.update({json_field: data})

            return parsers.DataAndFiles(qdict, result.files)

    return MultipartJsonParser

This is used like:

class MyFileViewSet(ModelViewSet):
    parser_classes = [gen_MultipartJsonParser(['tags', 'permissions'])]
    #                                           ^^^^^^^^^^^^^^^^^^^
    #                              Fields that need to be further JSON parsed
    ....
from rest_framework import status, generics
from rest_framework.response import Response
from rest_framework import serializers
import logging
logger = logging.getLogger(__name__)`enter code here`
class ImageUploadSerializer(serializers.Serializer):
    file = serializers.FileField()

class UploadImages(generics.GenericAPIView):
    
    serializer_class = ImageUploadSerializer
    permission_classes = [IsAuthenticated, ]

    def post(self, request):

        try:
            data = self.serializer_class(data=request.data)
            if data.is_valid() is False:
                return Response({'error': ERROR_MESSAGES.get('400')}, status=status.HTTP_400_BAD_REQUEST)
                is_file_upload_success, file_item = save_aws_article_image(data.validated_data.get('file'),
                                                                           request.user, upload_type)

            if is_file_upload_success:
                logger.info('{0} file uploaded {1}'.format(file_item['file_obj'].path, datetime.now()))
                return Response({'path': file_item['file_obj'].path, 'id': file_item['file_obj'].uuid,
                                 'name': file_item['file_obj'].name},
                                status=status.HTTP_201_CREATED)
        except Exception as e:
            logger.error(e, exc_info=True)
            return Response({"error": e}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

A DRF viewset fileupload example with React(axios) to send an audioBlob:

class MyViewSet(viewsets.ModelViewSet):
    parser_classes = (MultiPartParser, FormParser)
    queryset = MyModel.objects.all().order_by('created_at')
    serializer_class = MySerializer

serializer:

class MySerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel
        fields = '__all__'

model:

class MyModel(models.Model):
    sentence = models.ForeignKey(Sentence, related_name="voice_sentence", on_delete=models.CASCADE)
    voice_record = models.FileField(blank=True, default='')
    created_at = models.DateTimeField(auto_now_add=True)

axios:

export const sendSpeechText = async (audioBlob: any) => {
    const headers = {
        'Content-Type': 'application/json',
        'Content-Disposition': 'attachment; filename=audiofile.webm'
    }

    const audiofile = new File([audioBlob], "audiofile.webm", { type: "audio/webm" })

    const formData = new FormData();
    formData.append("sentence", '1');
    formData.append("voice_record", audiofile);

    return await axios.post(
        SEND_SPEECH_URL,
        formData,
        {
            crossDomain: true,
            headers: headers
        },
    )
}

NOTE: voice_record in formData should be the same in your model

There are majorly 3 ways for upload files in drf

suppose you have Tag model with title and logo fields and TagSerializer

class Tag(models.Model):
    title = models.CharField(max_length=10, default='')
    file = models.FileField(upload_to='tag/', blank=True, null=True, )


class TagSerializer(rest_serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = '__all__'

you can choose one of them according to your situation.

1- using serializer:

class UploadFile(APIView):
    parser_classes = (MultiPartParser, FormParser)

    def post(self, request):

        serializer = TagSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()

        return Response(serializer.data, status=status.HTTP_200_OK)

2- using write method :

def save_file(file: InMemoryUploadedFile, full_path):
    with open(full_path, 'wb+') as f:
        for chunk in file.chunks():
            f.write(chunk)


class UploadFile(APIView):
    parser_classes = (MultiPartParser, FormParser)

    def post(self, request):
        file: InMemoryUploadedFile = request.FILES['file']
        # define file_save_path variable 
        full_path = file_save_path + file.name
        save_file(file, full_path)
        return Response(serializer.data, status=status.HTTP_200_OK)

3- using FileSystemStorage:

class UploadFile(APIView):
parser_classes = (MultiPartParser, FormParser)

def post(self, request):
    file: InMemoryUploadedFile = request.FILES['file']
    f = FileSystemStorage()
    # this will save file in MEDIA_ROOT path
    f.save(file.name, file)
    return Response(serializer.data, status=status.HTTP_200_OK)

For the users who want to use or prefer a Function-Based Views for uploading files.

This is a Complete Guide from Creating Models > views > Serializers > URLs and Testing the endpoint with Postman. I have put the comments inside the code where required.

# models.py
# Imports 
from django.db import models
import os

def document_path_and_name(instance, filename):
    '''  Change the filename to 'instance_id + document_name '''
    ext = filename.split('.')[-1]
    filename = "%s_%s.%s" % (instance.id, instance.document_name, ext)

    ''' if document_name is 'doucment one' in pdf and id is 1
    then filname will be saved as = 1_document_one.pdf '''

    return os.path.join('files/', filename)

class Document(models.Model):
    # I'm using document_name and id to give the filename that would be save with
    # this using document_path_and_name function.
    # you can modify on your need. 
    document_name = models.CharField(max_length=100)
    file = models.FileField(upload_to=document_path_and_name)

    def __str__(self):
        return self.document_name

We don't need a Serializer to validate the file upload here but would need one if we need to serialize the response. So let's go with a simple ReadOnly Serializer in this case.

# serializers.py
# imports
from rest_framework import serializers

class DocumentSerializer(serializers.Serializer):
    id = serializers.IntegerField(read_only=True)
    document_name = serializers.CharField(read_only=True)
    file = serializers.URLField(read_only=True)

Now in the api_view, we will be using the MultiPartParser decorator to upload files via a POST request. We would need a document_name and a file for this function to upload the file correctly as we had set the Model.

# views.py
# imports
from rest_framework.decorators import api_view, parser_classes
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser
from .models import Document
from .serializers import DocumentSerializer

@api_view(['POST'])
@parser_classes([MultiPartParser])
def upload_document(request, filename, format=None):
    """
    A view that can accept POST requests with .media_type: multipart/form-data content.
    """
    file = request.FILES['file']
    doc = Document.objects.create(document_name=filename, file=file)
    # Do any thing else here
    serializer = DocumentSerializer(doc, many=False)
    return Response(serializer.data)

So, We will be passing document_name in the URL param, we can call it anything but I defined it as the filename. and our API ENDPOINT or Url will be like;

# imports
from django.urls import path
from .views import upload_document

urlpatterns = [
    path('upload_document/<str:filename>/', upload_document),
]

So to test this via Postman, go to your valid API endpoint like below enter image description here

I'm passing the filename for the document_name you can pass anything. You would notice that the actual file name is something else in pdf format in the screenshot below. That will be replaced with the help of our document_path_and_name function to id_document_name. So here the save filename is 1_filename.pdf

enter image description here

Now just make a request and your file will be uploaded to your directed file storage path. And you will get the JSON Response from DocumentSerializer. The main thing which was responsible for the file upload is the MultiPartParser decorator. Must visit the Docs for more details.

If you're using ViewSets, you can add a custom action to handle file uploads:

from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.parsers import FileUploadParser
from rest_framework import viewsets
...

class SomeViewSet(viewsets.ModelViewSet):
    serializer_class = ...
    permission_classes = [...]
    queryset = ...

    @action(methods=['put'], detail=True, parser_classes=[FileUploadParser])
    def upload_file(self, request, pk=None):
        obj = self.get_object()
        obj.file = request.data['file']
        obj.save()
        return Response(status=204)

This keeps everything within the ViewSet. You'll get an endpoint that looks something like this api/item/32/upload_file/.

The reason you'd use FileUploadParser as opposed to other options like multipart is if you're uploading from a native app for example and don't want to rely on a multi part encoder.

Related