django-ninja: Unable to upload file (always get 'field required' message)

Hi @vitalik, I’m trying to upload a file but it’s always getting a “field required” message.

Screenshot from 2021-08-15 08-55-14

Handler:

FileParam = File
def file_create(request: HttpRequest,
        name: str = FileParam(..., max_length=100),
        description: Optional[str] = FileParam(None, max_length=500),
        file: UploadedFile = FileParam(...),
        folder: Optional[UUID] = FileParam(None)
    ):

Has anything been missed?

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 21 (6 by maintainers)

Commits related to this issue

Most upvoted comments

@aprilahijriyan No, currently you have to pass files as arguments

class BodySupplierSchema(Schema):
    business_id: UUID
    name: constr(max_length=255)
    description: Optional[str] = None
    mobile_phone: Optional[str] = None
    address_details: Optional[str] = None

def create_supplier(request: HttpRequest, body: BodySupplierSchema = Form(...), image: Optional[UploadedFile] = File(None)):
    ...

@LexxLuey Found this. I guess PUT does not support multipart…? See Roy T. Fielding’s comment here

class Movie(models.Model):
    class Status_Choices(models.IntegerChoices):
        COMING_UP = 1
        STARTING = 2
        RUNNING = 3
        FINISHED = 4

    name = models.CharField(max_length=200, null=True)
    protagonists = models.CharField(max_length=200, null=True)
    poster = models.ImageField(upload_to ='posters/', null=True)
    trailer = models.FileField(upload_to ='trailers/', null=True)
    start_date = models.DateTimeField(auto_now=False, auto_now_add=False, null=True)
    status = models.IntegerField(choices=Status_Choices.choices, default=Status_Choices.COMING_UP)
    ranking = models.IntegerField(validators=[MinValueValidator(0, message="Cannot have ranking below 0")], default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    modified_at = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = "movie"
        verbose_name_plural = "movies"

    def __str__(self):
        return self.name

class MovieIn(ModelSchema):
    class Meta:
        model = Movie
        exclude = ["id", "created_at", "modified_at"]
        fields_optional = "__all__"


class MovieOut(ModelSchema):
    class Meta:
        model = Movie
        fields = "__all__"
        fields_optional = "__all__"


class Message(Schema):
    message: str


router = Router()


@router.post("/", response=MovieOut)
def create_movie(
    request,
    payload: MovieIn,
    poster_file: UploadedFile = None,
    trailer_file: UploadedFile = None,
):
    payload_dict = payload.dict()
    movie = Movie(**payload_dict)
    if not poster_file or trailer_file:
        movie.save()
    if poster_file:
        movie.poster.save(movie.name, poster_file)  # will save model instance as well
    if trailer_file:
        movie.trailer.save(movie.name, trailer_file)  # will save model instance as well
    return movie

@router.put("/{movie_id}", response=MovieOut)
def update_movie(
    request,
    movie_id: int,
    payload: MovieIn,
    poster_file: UploadedFile = None,
    trailer_file: UploadedFile = None,
):
    print(movie_id)
    print(payload)
    print(poster_file)
    print(trailer_file)
    movie = get_object_or_404(Movie, id=movie_id)
    for attr, value in payload.dict(exclude_unset=True).items():
        setattr(movie, attr, value)
    movie.save()
    if not poster_file or trailer_file:
        movie.save()
    if poster_file:
        movie.poster.save(movie.name, poster_file)  # will save model instance as well
    if trailer_file:
        movie.trailer.save(movie.name, trailer_file)  # will save model instance as well
    return movie

The POST method works fine whether you send a file or not when trying out the api in the swagger interactive docs. But it fails when it is a PUT method. This is the output I get in my terminal:

Unprocessable Entity: /api/cinema/2
[23/Mar/2024 12:27:48] "PUT /api/cinema/2 HTTP/1.1" 422 86
{
  "detail": [
    {
      "type": "missing",
      "loc": [
        "body",
        "payload"
      ],
      "msg": "Field required"
    }
  ]
}

the curl:

curl -X 'PUT' \
  'http://127.0.0.1:8000/api/cinema/2' \
  -H 'accept: application/json' \
  -H 'Content-Type: multipart/form-data' \
  -F 'poster_file=@GID_1127-DeNoiseAI-standard.jpg;type=image/jpeg' \
  -F 'trailer_file=@IMG-20230101-WA0129 (1).jpg;type=image/jpeg' \
  -F 'payload={
  "name": "3535353"
}'

Any help will be appreciated. Perhaps I am using the ninja wrong?

EDIT: I updated my PUT to this:

@router.put("/{movie_id}", response=MovieOut)
def update_movie(
    request,
    movie_id: int,
    payload: Form[MovieIn],
    poster_file: Optional[UploadedFile] = File(None),
    trailer_file: Optional[UploadedFile] = File(None),
):
    print(movie_id)
    print(payload)
    print(poster_file)
    print(trailer_file)
    movie = get_object_or_404(Movie, id=movie_id)
    for attr, value in payload.dict(exclude_unset=True).items():
        setattr(movie, attr, value)
    if not poster_file or trailer_file:
        movie.save()
    if poster_file:
        movie.poster.save(movie.name, poster_file)  # will save model instance as well
    if trailer_file:
        movie.trailer.save(movie.name, trailer_file)  # will save model instance as well
    return movie

in my terminal i get this:

2
name=None protagonists=None poster=None trailer=None start_date=None status=None ranking=None
None
None

the payload is always empty even though there is data sent from the client.

Any help would be very much appreciated

In my case, I had to read the file in binary mode :

    @cookielogin
    def assert_upload(self, url, file_path, expected_code, content_type="multipart/media-type"):
        with open(file_path, 'rb') as fp:
            binary_content = fp.read()
        simple_uploaded_file = SimpleUploadedFile(file_path, binary_content, content_type=content_type)
        response = self.client.post(url, {'file': simple_uploaded_file})
        self.raise_if_invalid_http_code(response, expected_code)
        return response.json()