How to Validate File Inputs in TypeScript with Zod

Form validation is an essential aspect of web development. It plays a crucial role in maintaining an application’s security and integrity. Forms are one of the exposed parts of an application because they accept external inputs from users. Through thes…


This content originally appeared on DEV Community and was authored by DrPrime01

Form validation is an essential aspect of web development. It plays a crucial role in maintaining an application’s security and integrity. Forms are one of the exposed parts of an application because they accept external inputs from users. Through these external form inputs, malicious users or attackers can enter malicious scripts or files that can destroy the integrity of your application and access your users' private data.
Although text-based inputs are the primary targets for attackers attempting to inject malicious scripts into an application, file inputs—not directly vulnerable to script injections—are the most disguised point of entry and are less common. Attackers can use file uploads to embed malicious scripts in ways that are not immediately obvious.
Form validation is a way of preventing these malicious scripts in your application. Zod is a schema validation library for JavaScript and TypeScript that helps enforce data structures and validate inputs. Unlike typical validation libraries, Zod integrates seamlessly with TypeScript, providing strong type safety at runtime. In this article, you’ll learn how to validate your file inputs with Zod, a schema validation library for TypeScript/JavaScript.

Zod’s Overview

Zod is a TypeScript-first schema declaration and validation library that mimics TypeScript’s strong static typing. It allows you to enforce type safety at runtime, making it an excellent choice for TypeScript developers who want strong typing and reliable validation without compromising either.
Zod provides a powerful and flexible way to define schemas for your data structures. Its schema’s syntax is straightforward and you only need to use the ‘z’ object imported from ‘zod’. The ‘z’ object includes methods for various data types such as:


import { z } from "zod";

// Basic primitive schemas
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
const undefinedSchema = z.undefined();
const nullSchema = z.null();

// Object Schema
const userSchema = z.object({
  name: z.string(),
  age: z.number().int(), //built-in method: Only allows integers
  email: z.string().email(), //built-in method: Validates email format
});

// Array schema
const stringArraySchema = z.array(z.string());

// Optional and Nullable types
const userSchema = z.object({
  name: z.string(),
  age: z.number().int().optional(), // age is optional
  email: z.string().email().nullable(), // email can be null
});

Project Setup

For this article, you need two dependencies: React and Zod. Create a Typescript variant of a React application with Vite and install Zod.

// create react app with vite
npm create vite@latest file-input-validation -- --template react-ts
cd file-input-validation
npm install

// install zod
npm install zod

After running the above command in your terminal, open the file-input-validation directory in your code editor, and run npm run dev to kickstart the application in localhost.
Now that your application is running successfully, delete the App.css file, clear out the App.tsx and index.css files, and replace the former with the code block below.
As for the styles, see it here.


// App.tsx

/* eslint-disable @typescript-eslint/no-explicit-any */
import { DOCUMENT_SCHEMA, IMAGE_SCHEMA } from "./utils/schema";
import { useState, useEffect } from "react";
interface ErrorType {
  img_upload?: string;
  doc_upload?: string;
}
function App() {
  const [docFile, setDocFile] = useState<File | undefined>();
  const [imgFile, setImgFile] = useState<File | undefined>();
  const [imgUrl, setImgUrl] = useState("");
  const [error, setError] = useState<ErrorType>({});
  useEffect(() => {
    if (imgFile) {
      const url = URL.createObjectURL(imgFile);
      setImgUrl(url);
      return () => URL.revokeObjectURL(url);
    }
  }, [imgFile]);

  const handleDocChange = (e: React.ChangeEvent<HTMLInputElement>) => {

  };
  const handleImgChange = (e: React.ChangeEvent<HTMLInputElement>) => {

  };
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

  };
  return (
    <div className="app-container">
      <h1>File Input Validation with Zod</h1>
      <div className="form-container">
        <form className="form" onSubmit={handleSubmit}>
          <div className="formfield">
            <label htmlFor="doc-input">
              <p>Document Input</p>
              <div className="doc-label">
                {docFile?.name ? (
                  <p>{docFile?.name}</p>
                ) : (
                  <p>
                    <span>Browse</span> to upload document here{" "}
                  </p>
                )}
                <p className="size">(5MB Max)</p>
              </div>
            </label>
            <input
              id="doc-input"
              name="doc_upload"
              type="file"
              onChange={handleDocChange}
              accept="application/*"
            />
            {error.doc_upload && <p className="error">{error.doc_upload}</p>}
          </div>
          <div className="formfield">
            <label htmlFor="img-input">
              <p>Image Input</p>
              <div className="image-label">
                {imgUrl ? (
                  <img src={imgUrl} alt="img-input" />
                ) : (
                  <div>
                    <p>
                      <span>Browse</span> to upload image here{" "}
                    </p>
                    <p className="size">(5MB Max)</p>
                  </div>
                )}
              </div>
            </label>
            <input
              id="img-input"
              name="img_upload"
              type="file"
              accept="image/*"
              onChange={handleImgChange}
            />
            {error.img_upload && <p className="error">{error.img_upload}</p>}
          </div>
          <button
            type="submit"
            disabled={
              !!error.doc_upload || !!error.img_upload || !docFile || !imgFile
            }
          >
            Submit
          </button>
        </form>
      </div>
    </div>
  );
}
export default App;

For now, leave the functions empty. You’ll fill them up later as you proceed.

Validating File Inputs

The HTML file element allows any file type including video, audio, image, and documents. When defining your file inputs, you can dictate the file types it should receive using the accept attribute—perhaps only image files or a combination of video and audio files. The accept attribute takes a string value of any valid unique file type identifier and prevents the file input from accepting any file with a type not corresponding to the specified type identifier. Using the accept attribute, you can restrict file types to exclude dangerous formats like executable files (.exe), scripts (.js), and macro-enabled documents (.docm).

File Type Validation

With Zod, you can also define the file types your input should accept as an extra layer of security. In your App.tsx file are two file inputs—one for images and the other for documents, and you’ll write a Zod schema to validate each with the instanceof and refine methods.


import { z } from "zod";

// Document Schema
export const DOCUMENT_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "application/pdf",
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      ].includes(file.type),
    { message: "Invalid document file type" }
  );

// Image Schema
export const IMAGE_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "image/png",
        "image/jpeg",
        "image/jpg",
        "image/svg+xml",
        "image/gif",
      ].includes(file.type),
    { message: "Invalid image file type" }
  );

The instanceof method checks if a value is an instance of a specific class. In this case, both schemas check if the value is of the TypeScript File class. The refine method allows you to define custom validation logic. It takes 2 arguments—a callback function expected to return a false value for a failed validation check and an object for passing customisable options for error-handling behaviours.
Zod takes in the File value and checks the type with the types listed in the array. If the file type is in the array, the file passes the validation. If not, Zod sends the error message.

File Size Validation

Besides the file type validation, you can augment the schema to validate the file size using the refine method again.


import { z } from "zod";
const fileSizeLimit = 5 * 1024 * 1024; // 5MB

// Document Schema
export const DOCUMENT_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "application/pdf",
        "application/vnd.ms-excel",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      ].includes(file.type),
    { message: "Invalid document file type" }
  )
  .refine((file) => file.size <= fileSizeLimit, {
    message: "File size should not exceed 5MB",
  });

// Image Schema
export const IMAGE_SCHEMA = z
  .instanceof(File)
  .refine(
    (file) =>
      [
        "image/png",
        "image/jpeg",
        "image/jpg",
        "image/svg+xml",
        "image/gif",
      ].includes(file.type),
    { message: "Invalid image file type" }
  )
  .refine((file) => file.size <= fileSizeLimit, {
    message: "File size should not exceed 5MB",
  });

Just like the file type is checked with the refine method, the same method is used to check the file size by comparing it with the fileSizeLimit defined in the code block above. If the file size is over the limit, Zod returns the defined error message.
In this way, you have coupled two validation checks in one schema to validate the file type and size.

Frontend Validation with Zod Schemas

The schemas are useless unless integrated with the form that provides the values to be validated. In the App.tsx file, there are undefined functions, and you will define them in this section.
First and foremost, import your schemas into the App.tsx file, then define the validation functions.


import { DOCUMENT_SCHEMA, IMAGE_SCHEMA } from "./utils/schema";

const validateFile = (file: File, schema: any, field: keyof ErrorType) => {
    const result = schema.safeParse(file);
    if (!result.success) {
      setError((prevError) => ({
        ...prevError,
        [field]: result.error.errors[0].message,
      }));
      return false;
    } else {
      setError((prevError) => ({
        ...prevError,
        [field]: undefined,
      }));
      return true;
    }
  };
  const handleDocChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const isValid = validateFile(file, DOCUMENT_SCHEMA, "doc_upload");
      if (isValid) setDocFile(file);
    }
  };
  const handleImgChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const isValid = validateFile(file, IMAGE_SCHEMA, "img_upload");
      if (isValid) setImgFile(file);
    }
  };

The validateFile function takes 3 arguments—the file, the schema, and the field. The schema safeParse’s the file and the result is stored in the result variable. If the parse is unsuccessful, the function updates the field of the failed input in the error state with the error message Zod returns and the function returns false. If successful, the field is set to undefined in the error state and the function returns true. By using the Zod safeParse method, you allow your application to fail gracefully, preventing Zod from throwing errors, which could be a bad user experience.
handleDocChange and handleImgChange are the functions you pass to the doc_upload and img_upload inputs respectively. The functions monitor the files uploaded to their respective input files and run the validateFile function on them, storing the response in the isValid variable. If the variable is true, the functions update their respective states with the files.
Finally, the error state helps to store the error information about each input field. This information is displayed in the <p> tag below each input field whenever any error state object property contains an error which corresponds to it.

Handling Multiple Files

By adding the multiple attribute to your input element, you can select and upload multiple files at once. With Zod, you can write a validation schema for a FileList which is as easy as doing the same for a File.


const fileSizeLimit = 5 * 1024 * 1024; // 5MB

export const fileUploadSchema = z.object({
  files: z
    .instanceof(FileList)
    .refine((list) => list.length > 0, "No files selected")
    .refine((list) => list.length <= 5, "Maximum 5 files allowed")
    .transform((list) => Array.from(list))
    .refine(
      (files) => {
        const allowedTypes: { [key: string]: boolean } = {
          "image/jpeg": true,
          "image/png": true,
          "application/pdf": true,
          "application/msword": true,
          "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
            true,
        };
        return files.every((file) => allowedTypes[file.type]);
      },
      { message: "Invalid file type. Allowed types: JPG, PNG, PDF, DOC, DOCX" }
    )
    .refine(
      (files) => {
        return files.every((file) => file.size <= fileSizeLimit);
      },
      {
        message: "File size should not exceed 5MB",
      }
    ),
});

There are two new Zod methods in this schema—object and transform. The object method allows you to define a validation schema for an object and each property of the object. The transform method allows you to convert a value from one form to the other. In this case, the FileList is converted to an array for easier manipulation.
With this schema defined, you can integrate it with your application’s form input elements as in the previous section.


const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const selectedFiles = e.target.files;
  if (!selectedFiles) return;
  const result = fileUploadSchema.safeParse({ files: selectedFiles });
  if (result.success) {
    setFiles(result.data.files);
    setError(null);
  } else {
    setError(result.error.errors[0].message);
    setFiles([]);
  }
};

Preventing Security Risks and Handling Edge Cases

One of the main purposes of file validation is to secure your application against potential security risks that could include uploading some malicious scripts hidden in files. Even with a limitation on the types of accepted files, it's highly important to take additional measures to protect against them:

  • Validate File Type and MIME Type: Make sure the uploaded files match their MIME type - such as application/pdf - and extension - such as .pdf. Some file types, such as .svg files, may contain scripts and must therefore be carefully validated or sanitized if they are to be permitted.
  • Clean and Limit Content Size: Where file types can be utilized to contain scripts, such as .svg or .docx, for example, you may consider using server-side sanitization libraries to exorcise any potentially harmful content.
  • Rejected File Types for Suspicious Ones: If the file type is ambiguous, or if its MIME type doesn't correspond with the extension of the file, reject it and an graceful error message will be displayed to the user. This is a precautionary measure, which will prevent certain kinds of attack vectors, such as disguised executables.

Apart from security, file uploads do have many edge cases that need to be handled well in advance. The following are a few examples:

  • Empty File Uploads: Users sometimes don't even notice when they select an empty file meaning one with 0 bytes of size. Zod can validate against this by having a minimum size requirement. A File which is Empty should be rejected because there may be an error in actually processing or displaying its content.
  • Unexpected File Encodings: Some files use unusual encoding; this may cause errors in other applications. You can log or reject the files containing unexpected encoding.
  • Intermittent Upload Errors: In real-world applications, users might face network interruptions or other issues during file uploading. You can include retry mechanisms or error prompts so that users can complete their uploads.

In summary, make your validation system robust and user-friendly. Also, bear in mind security risks and common edge cases to ensure the users' interactions with your application's file-handling features go smoothly, error-free, and safely.

Conclusion

Adding file upload validation by Zod creates an extra layer of security and further improves the user experience for your application. Zod validation schema makes it easy to implement comprehensive checks, and provide clear feedback to users. This feedback allows your users to understand and resolve the issues in their values quickly.
As you iterate on your validation logic, you're actively building a safer application that instils more trust and satisfaction in your user. Zod gives flexibility in handling file validation to set such standards out of the box with a solid base for any project needing file-handling functionality.
For a more comprehensive section about Zod, see the documentation

See the full project of this article here.


This content originally appeared on DEV Community and was authored by DrPrime01


Print Share Comment Cite Upload Translate Updates
APA

DrPrime01 | Sciencx (2024-11-01T05:06:55+00:00) How to Validate File Inputs in TypeScript with Zod. Retrieved from https://www.scien.cx/2024/11/01/how-to-validate-file-inputs-in-typescript-with-zod/

MLA
" » How to Validate File Inputs in TypeScript with Zod." DrPrime01 | Sciencx - Friday November 1, 2024, https://www.scien.cx/2024/11/01/how-to-validate-file-inputs-in-typescript-with-zod/
HARVARD
DrPrime01 | Sciencx Friday November 1, 2024 » How to Validate File Inputs in TypeScript with Zod., viewed ,<https://www.scien.cx/2024/11/01/how-to-validate-file-inputs-in-typescript-with-zod/>
VANCOUVER
DrPrime01 | Sciencx - » How to Validate File Inputs in TypeScript with Zod. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/11/01/how-to-validate-file-inputs-in-typescript-with-zod/
CHICAGO
" » How to Validate File Inputs in TypeScript with Zod." DrPrime01 | Sciencx - Accessed . https://www.scien.cx/2024/11/01/how-to-validate-file-inputs-in-typescript-with-zod/
IEEE
" » How to Validate File Inputs in TypeScript with Zod." DrPrime01 | Sciencx [Online]. Available: https://www.scien.cx/2024/11/01/how-to-validate-file-inputs-in-typescript-with-zod/. [Accessed: ]
rf:citation
» How to Validate File Inputs in TypeScript with Zod | DrPrime01 | Sciencx | https://www.scien.cx/2024/11/01/how-to-validate-file-inputs-in-typescript-with-zod/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.