[Django]-Using Django Rest Framework, how can I upload a file AND send a JSON payload?

24👍

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 can have a single key called data and send the whole json as string in value.

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)
    queryset = Posts.objects.all()
    lookup_field = 'id'

You will need custom parser as shown below for parsing json.

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 = {}
        # 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)

The request example in postman case2

EDIT:

see this extended answer if you want to send each data as key value pair

👤Nithin

13👍

I know this is an old thread, but I just came across this. I had to use MultiPartParser in order to get my file and extra data to come across together. Here’s what my code looks like:

# views.py
class FileUploadView(views.APIView):
    parser_classes = (MultiPartParser,)

    def put(self, request, filename, format=None):
        file_obj = request.data['file']
        ftype    = request.data['ftype']
        caption  = request.data['caption']
        # ...
        # do some stuff with uploaded file
        # ...
        return Response(status=204)

My AngularJS code using ng-file-upload is:

file.upload = Upload.upload({
  url: "/api/picture/upload/" + file.name,
  data: {
    file: file,
    ftype: 'final',
    caption: 'This is an image caption'
  }
});

6👍

I send JSON and an image to create/update a product object. Below is a create APIView that works for me.

Serializer

class ProductCreateSerializer(serializers.ModelSerializer):
    class Meta:
         model = Product
        fields = [
            "id",
            "product_name",
            "product_description",
            "product_price",
          ]
    def create(self,validated_data):
         return Product.objects.create(**validated_data)

View

from rest_framework  import generics,status
from rest_framework.parsers import FormParser,MultiPartParser

class ProductCreateAPIView(generics.CreateAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductCreateSerializer
    permission_classes = [IsAdminOrIsSelf,]
    parser_classes = (MultiPartParser,FormParser,)

    def perform_create(self,serializer,format=None):
        owner = self.request.user
        if self.request.data.get('image') is not None:
            product_image = self.request.data.get('image')
            serializer.save(owner=owner,product_image=product_image)
        else:
            serializer.save(owner=owner)

Example test:

def test_product_creation_with_image(self):
    url = reverse('products_create_api')
    self.client.login(username='testaccount',password='testaccount')
    data = {
        "product_name" : "Potatoes",
        "product_description" : "Amazing Potatoes",
        "image" : open("local-filename.jpg","rb")
    }
    response = self.client.post(url,data)
    self.assertEqual(response.status_code,status.HTTP_201_CREATED)

4👍

@Nithin solution works but essentially it means you are sending JSON as strings and hence not using the actual application/json inside the multipart segments.

What we want is to make the backend accept data in the below format

------WebKitFormBoundaryrga771iuUYap8BB2
Content-Disposition: form-data; name="file"; filename="1x1_noexif.jpeg"
Content-Type: image/jpeg


------WebKitFormBoundaryrga771iuUYap8BB2
Content-Disposition: form-data; name="myjson"; filename="blob"
Content-Type: application/json

{"hello":"world"}
------WebKitFormBoundaryrga771iuUYap8BB2
Content-Disposition: form-data; name="isDownscaled"; filename="blob"
Content-Type: application/json

false
------WebKitFormBoundaryrga771iuUYap8BB2--

MultiPartParser works with the above format but will treat those jsons as files. So we simply unmarshal those jsons by putting them to data.

parsers.py

from rest_framework import parsers

class MultiPartJSONParser(parsers.MultiPartParser):
    def parse(self, stream, *args, **kwargs):
        data = super().parse(stream, *args, **kwargs)

        # Any 'File' found having application/json as type will be moved to data
        mutable_data = data.data.copy()
        unmarshaled_blob_names = []
        json_parser = parsers.JSONParser()
        for name, blob in data.files.items():
            if blob.content_type == 'application/json' and name not in data.data:
                mutable_data[name] = json_parser.parse(blob)
                unmarshaled_blob_names.append(name)
        for name in unmarshaled_blob_names:
            del data.files[name]
        data.data = mutable_data

        return data

settings.py

REST_FRAMEWORK = {
    ..
    'DEFAULT_PARSER_CLASSES': [
        ..
        'myproject.parsers.MultiPartJSONParser',
    ],
}

This should work now.

The final bit is testing. Since the test client that ships with Django and REST doesn’t support multipart JSON, we work around that by wrapping any JSON data.

import io
import json

def JsonBlob(obj):
    stringified = json.dumps(obj)
    blob = io.StringIO(stringified)
    blob.content_type = 'application/json'
    return blob

def test_simple(client, png_3x3):
    response = client.post(f'http://localhost/files/', {
            'file': png_3x3,
            'metadata': JsonBlob({'lens': 'Sigma 35mm'}),
        }, format='multipart')
    assert response.status_code == 200

3👍

It is very simple to use a multipart post and a regular view, if this is an option.

You send the json as a field and files as files, then process in one view.

Here is a simple python client and a Django server:

Client – sending multiple files and an arbitrary json-encoded object:

import json
import requests

payload = {
    "field1": 1,
    "manifest": "special cakes",
    "nested": {"arbitrary":1, "object":[1,2,3]},
    "hello": "word" }

filenames = ["file1","file2"]
request_files = {}
url="example.com/upload"

for filename in filenames:
    request_files[filename] = open(filename, 'rb')

r = requests.post(url, data={'json':json.dumps(payload)}, files=request_files)

Server – consuming the json and saving the files:

@csrf_exempt
def upload(request):
    if request.method == 'POST':
        data = json.loads(request.POST['json']) 
        try:
            manifest = data['manifest']
            #process the json data

        except KeyError:
            HttpResponseServerError("Malformed data!")

        dir = os.path.join(settings.MEDIA_ROOT, "uploads")
        os.makedirs(dir, exist_ok=True)

        for file in request.FILES:
            path = os.path.join(dir,file)
            if not os.path.exists(path):
                save_uploaded_file(path, request.FILES[file])           

    else:
        return HttpResponseNotFound()

    return HttpResponse("Got json data")


def save_uploaded_file(path,f):
    with open(path, 'wb+') as destination:
        for chunk in f.chunks():
            destination.write(chunk)
👤QT-1

3👍

If you’re getting an error along the lines of Incorrect type. Expected pk value, received list., with @nithin’s solution, it’s because Django’s QueryDict is getting in the way – it’s specifically structured to use a list for each entry in the dictionary, and thus:

{ "list": [1, 2] }

when parsed by MultipartJsonParser yields

{ 'list': [[1, 2]] }

which trips up your serializer.

Here is an alternative which handles this case, specifically expecting the _data key for your JSON:

from rest_framework import parsers
import json

class MultiPartJSONParser(parsers.MultiPartParser):
    def parse(self, stream, *args, **kwargs):
        data = super().parse(stream, *args, **kwargs)
        json_data_field = data.data.get('_data')
        if json_data_field is not None:
            parsed = json.loads(json_data_field)
            mutable_data = {}
            for key, value in parsed.items():
                mutable_data[key] = value
            mutable_files = {}
            for key, value in data.files.items():
                if key != '_data':
                    mutable_files[key] = value
            return parsers.DataAndFiles(mutable_data, mutable_files)

        json_data_file = data.files.get('_data')
        if json_data_file:
            parsed = parsers.JSONParser().parse(json_data_file)
            mutable_data = {}
            for key, value in parsed.items():
                mutable_data[key] = value
            mutable_files = {}
            for key, value in data.files.items():
                mutable_files[key] = value
            return parsers.DataAndFiles(mutable_data, mutable_files)

        return data

1👍

I’d just like to add to @Pithikos’s answer by modifying the parser to accept lists as well, in line with how DRF parses lists in serializers in utils/html#parse_html_list

class MultiPartJSONParser(parsers.MultiPartParser):
    def parse(self, stream, *args, **kwargs):
        data = super().parse(stream, *args, **kwargs)

        # Any 'File' found having application/json as type will be moved to data
        mutable_data = data.data.copy()
        unmarshaled_blob_names = []
        json_parser = parsers.JSONParser()
        for name, blob in data.files.items():
            if blob.content_type == 'application/json' and name not in data.data:
                parsed = json_parser.parse(blob)
                if isinstance(parsed, list):
                    # need to break it out into [0], [1] etc
                    for idx, item in enumerate(parsed):
                        mutable_data[name+f"[{str(idx)}]"] = item
                else:
                    mutable_data[name] = parsed
                unmarshaled_blob_names.append(name)
        for name in unmarshaled_blob_names:
            del data.files[name]
        data.data = mutable_data

        return data

0👍

The following code worked for me.

from django.core.files.uploadedfile import SimpleUploadedFile
import requests
from typing import Dict

with open(file_path, 'rb') as f:
    file = SimpleUploadedFile('Your-Name', f.read())

    data: Dict[str,str]
    files: Dict[str,SimpleUploadedFile] = {'model_field_name': file}

    requests.put(url, headers=headers, data=data, files=files)
    requests.post(url, headers=headers, data=data, files=files)

'model_field_name' is the name of the FileField or ImageField in your Django model. You can pass other data as name or location as usual by using data parameter.

Hope this helps.

0👍

This work for me:

class FileUpload(APIView):
    parser_classes = [MultiPartParser]
    authentication_classes = [JWTAuthentication]

    def post(self, request, filename, format=None):
       file = request.data['file']
       data = json.loads(request.POST['form'])
       #.... just do.....

.
.
.

frontend part: example with fetch (vue frontend)

let data = await new FormData(); // creates a new FormData object
data.append("file", this.files); // add your file to form data
data.append('form',JSON.stringify(body)) //add your json

fetch(`https://endpoint/FileUpload/${body.nombre}`, {
  method: 'POST',
  body: data, 
  headers: {Authorization: `Bearer ${accessToken}`}
              })

I hope this helps.

0👍

I was just writing another question on SO because none of the answers above were working for me.

I somehow found out, that for nested serializers, DRF accepts values in dotted notation. So if you have a Location serializer with x and y values, you can set the name of the field in your form to location.x and location.y and it will work.

If you want to send JSON, you can use this serializer, it will work.

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

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 nested serializers, drf accepts values in dotted notaion (if sent as Multipart/Formdata). E.g if location is nested serializer.
    # It will accept location.x and location.y if data is to be entered in form fields.
    # the 2 nested for loops, ensures that the JSON data sent in form field is converted to the above format.
    #e.g if the key is asset_location. and it has x and y keys inside. It will be converted to asset_location.x, and asset_location.y


    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)
               
                if type(data[key]) == dict:
                    for inner_key,inner_value in data[key].items():
                        data[f'{key}.{inner_key}']=inner_value
            except ValueError:
                data[key] = value
        else:
            data[key] = value

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

Leave a comment