Validate an Image File with Zod

If you're working with image file uploads in your web application, ensuring that the files meet specific requirements is pretty common. I like to validate/test my form schema and checks using Zod.

I recently needed to use a Zod schema that also had some image requirements, so let me share how I made a fairly robust image check:

The Validation

Before jumping into the code, let's outline the validation requirements for my image files. It was being added to an object so you'll see I've added to the schema object. You can easily alter the logic to suit your needs.

  1. File Size: The image should be no larger than 5MB.
  2. File Type: Only JPEG, JPG, PNG, and WebP image types are allowed.
  3. Image Dimensions: The image dimensions should be between 200x200 pixels and 4096x4096 pixels.

Obviously, you can change this to suit your needs, but before proceeding, I wanted to share my constraints.

We need to create a Zod schema that checks these conditions to achieve our validation goals. Here's how we can do it:

import { z } from "zod";

const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const MIN_DIMENSIONS = { width: 200, height: 200 };
const MAX_DIMENSIONS = { width: 4096, height: 4096 };
const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/jpg", "image/png", "image/webp"];

const formatBytes = (bytes: number, decimals = 2) => {
  if (bytes === 0) return "0 Bytes";
  const k = 1024;
  const dm = decimals < 0 ? 0 : decimals;
  const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
};

export const schema = z
  .object({
    image: z
      .instanceof(File, {
        message: "Please select an image file.",
      })
      .refine((file) => file.size <= MAX_FILE_SIZE, {
        message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`,
      })
      .refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
        message: "Please upload a valid image file (JPEG, PNG, or WebP).",
      })
      .refine(
        (file) =>
          new Promise((resolve) => {
            const reader = new FileReader();
            reader.onload = (e) => {
              const img = new Image();
              img.onload = () => {
                const meetsDimensions =
                  img.width >= MIN_DIMENSIONS.width &&
                  img.height >= MIN_DIMENSIONS.height &&
                  img.width <= MAX_DIMENSIONS.width &&
                  img.height <= MAX_DIMENSIONS.height;
                resolve(meetsDimensions);
              };
              img.src = e.target?.result as string;
            };
            reader.readAsDataURL(file);
          }),
        {
          message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`,
        }
      ),
  });

Breaking Down the Code

Let's break down the code to understand each part of the validation:

  1. File Size Check:

    .refine((file) => file.size <= MAX_FILE_SIZE, {
      message: `The image is too large. Please choose an image smaller than ${formatBytes(MAX_FILE_SIZE)}.`,
    })
    

    This ensures the file size does not exceed 5MB. The formatBytes function is a helper to convert the file size into a readable format.

  2. File Type Check:

    .refine((file) => ACCEPTED_IMAGE_TYPES.includes(file.type), {
      message: "Please upload a valid image file (JPEG, PNG, or WebP).",
    })
    

    This checks if the file type is one of the accepted image formats.

  3. Image Dimensions Check:

    .refine(
      (file) =>
        new Promise((resolve) => {
          const reader = new FileReader();
          reader.onload = (e) => {
            const img = new Image();
            img.onload = () => {
              const meetsDimensions =
                img.width >= MIN_DIMENSIONS.width &&
                img.height >= MIN_DIMENSIONS.height &&
                img.width <= MAX_DIMENSIONS.width &&
                img.height <= MAX_DIMENSIONS.height;
              resolve(meetsDimensions);
            };
            img.src = e.target?.result as string;
          };
          reader.readAsDataURL(file);
        }),
      {
        message: `The image dimensions are invalid. Please upload an image between ${MIN_DIMENSIONS.width}x${MIN_DIMENSIONS.height} and ${MAX_DIMENSIONS.width}x${MAX_DIMENSIONS.height} pixels.`,
      }
    )
    

    This part is a bit more complex. It reads the file as a data URL and creates an image object to check its dimensions. If the dimensions fall within the specified range, it resolves the promise.

By setting up a schema with specific rules for file size, type, and dimensions, you can ensure that the uploaded images meet your needs.

Zod
Avatar for Niall Maher

Written by Niall Maher

Founder of Codú - The web developer community! I've worked in nearly every corner of technology businesses: Lead Developer, Software Architect, Product Manager, CTO, and now happily a Founder.

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.