node.jsexpressmulterzod

Zod Schema Validation of FormData with multiple Files failing


I'm trying to use Zod to perform Schema Validation on a FormData Object which has multiple files (HTML file input element with multi-select) in an Express App. When the Schema is validated, it appears the files property is undefined. I'm hoping someone can provide some guidance as to why the property doesn't seem to exist in the request.

HTML element

<input type="file" id="files" name="files" multiple />

JavaScript

// Create a new FormData object
const formData = new FormData();

// Append the form data to the FormData object
formData.append("title", $("#title").val());
formData.append("content", $("#content").val());

// Append the files to the FormData object
const files = document.getElementById("files").files;
for (let i = 0; i < files.length; i++) {
    formData.append("files", files[i]);
}

// Use fetch to post the FormData object to the server
// This will automatically set the Content-Type header to multipart/form-data
const response = await fetch("http://localhost:3001/blog", {
    method: "POST",
    body: formData,
});
console.log(await response.json());

Express Route

import express from "express";
import multer from "multer";

import { validate } from "../middleware/schemaValidator.js";
import { blogSchema } from "../schemas/blogSchema.js";
import { addBlogPost } from "../controllers/blogController.js";

const storage = multer.diskStorage({
  destination: (_req, _file, cb) => {
    cb(null, "public/uploads/");
  },
  filename: (_req, file, cb) => {
    cb(null, file.originalname);
  },
});
const upload = multer({ storage });

const router = express.Router();

// Validate the blogSchema before adding a new blog post
// The validate function will return an error if the request body does not match the schema
// If there is no error, the addBlogPost function will be called
router.post("/", upload.array("files", 2), validate(blogSchema), addBlogPost);

export default router;

Zod Schema

import { z } from "zod";
import { zfd } from "zod-form-data";

const ACCEPTED_IMAGE_TYPES = ["image/png"];

export const blogSchema = zfd.formData({
  title: z.string(),
  content: z.string(),
  files: z.array(z.instanceof(File)).refine((files) => {
    return files.every((file) => ACCEPTED_IMAGE_TYPES.includes(file.type));
  }),
});

Error

[
    {
      "code": "invalid_type",
      "expected": "array",
      "received": "undefined",
      "path": [
        "files"
      ],
      "message": "Required"
    }
]

GitHub Repo with complete code

Thanks in advance for any guidance!


Solution

  • The problem is that you pass req.body to your validation middleware here:

    // Validate the request body against the schema
    schema.parse(req.body);
    

    but multer stores files in req.files, which is why it's undefined, while other fields are in req.body, which is why it works when files are not present. So, you'd need to include req.files in validation.

    Also, multer doesn't use File types, it creates custom objects, you can see their properties here in the docs: File information.

    So, you could instead validate files as array of objects.

    Try this:

    add files to the validator:

    schema.parse({...req.body, files:req.files});
    

    add validation against multer objects (array of two multer objects):

    const multerFile = z.object({
        fieldname: z.string(),
        originalname: z.string(),
        encoding: z.string(),
        mimetype: z.string(),
        destination: z.string(),
        filename: z.string(),
        path: z.string(),
        size: z.number()
    });
    
    
    export const blogSchema = zfd.formData({
        title: z.string(),
        content: z.string(),
        // max 2 files, modify as needed
        files: z.array(multerFile).max(2).refine((files) => {
            return files.every((file) => ACCEPTED_IMAGE_TYPES.includes(file.mimetype));
        })
    });