npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@typed-web/valibot-form-data

v0.2.1

Published

Valibot validators for parsing and validating FormData

Readme

@typed-web/valibot-form-data

A set of Valibot validators for parsing and validating FormData and URLSearchParams objects. It's inspired by Zod's zod-form-data package but built with Valibot.

Installation

npm install @typed-web/valibot-form-data valibot
pnpm add @typed-web/valibot-form-data valibot
yarn add @typed-web/valibot-form-data valibot

Why?

Working with HTML forms in JavaScript can be tedious. FormData values are always strings (or Files), and you often need to:

  • Convert empty strings to undefined for optional fields
  • Coerce numeric strings to numbers
  • Handle checkboxes that may or may not be present
  • Process file uploads with empty file detection
  • Deal with multiple values for the same field name
  • Parse nested object structures from dot/bracket notation

This library provides a set of helpers that make these common tasks simple and type-safe.

Features

  • Type-safe: Full TypeScript support with proper type inference
  • Valibot-based: Leverages Valibot's composable validation system
  • FormData & URLSearchParams: Works with both web standard APIs
  • Empty string handling: Automatically treats empty strings as undefined
  • Numeric coercion: Converts string numbers to actual numbers
  • Checkbox support: Handles checkbox on/off states
  • File uploads: Treats empty files as undefined
  • Repeatable fields: Handles multiple values for the same field name
  • Nested objects: Parses dot notation (e.g., address.street), bracket notation (e.g., items[0][name]), and mixed notation into nested object structures

Basic Usage

import * as v from "valibot";
import { formData, text, numeric, checkbox } from "@typed-web/valibot-form-data";

const schema = formData({
  name: text(), // Required text field
  email: text(v.pipe(v.string(), v.email())), // Required email
  age: numeric(v.optional(v.number())), // Optional number
  subscribe: checkbox(), // Checkbox (true/false)
});

// Parse FormData from an HTML form
const form = document.querySelector("form");
const data = new FormData(form);
const result = v.parse(schema, data);
// → { name: "John", email: "[email protected]", age: 25, subscribe: true }

API Reference

formData(shape)

Main function for processing FormData or URLSearchParams into a structured object. Supports nested objects using v.object().

const schema = formData({
  name: text(),
  address: v.object({
    street: text(),
    city: text(),
  }),
});

// FormData input with dot notation:
// name=John&address.street=123 Main St&address.city=NYC
// Result: { name: "John", address: { street: "123 Main St", city: "NYC" } }

text(schema?)

Schema for text input fields. Transforms empty strings to undefined before validation.

// Required field
const required = text(); // or text(v.string())
text().parse(""); // → ValidationError
text().parse("Hello"); // → "Hello"

// Optional field
const optional = text(v.optional(v.string()));
optional.parse(""); // → undefined
optional.parse("Hello"); // → "Hello"

// With validation
const minLength = text(v.pipe(v.string(), v.minLength(3)));
minLength.parse("ab"); // → ValidationError
minLength.parse("abc"); // → "abc"

numeric(schema?)

Schema for numeric input fields. Coerces numerical strings to numbers and transforms empty strings to undefined.

// Required number
const required = numeric(); // or numeric(v.number())
numeric().parse(""); // → ValidationError
numeric().parse("42"); // → 42

// Optional number
const optional = numeric(v.optional(v.number()));
optional.parse(""); // → undefined
optional.parse("42"); // → 42

// With validation
const minValue = numeric(v.pipe(v.number(), v.minValue(13)));
minValue.parse("10"); // → ValidationError
minValue.parse("15"); // → 15

checkbox(args?)

Schema for checkbox inputs. Converts form values to boolean.

// Default: treats "on" as true
const defaultCheckbox = checkbox();
defaultCheckbox.parse("on"); // → true
defaultCheckbox.parse(undefined); // → false

// Custom true value
const customCheckbox = checkbox({ trueValue: "yes" });
customCheckbox.parse("yes"); // → true
customCheckbox.parse(undefined); // → false

file(schema?)

Schema for file input fields. Transforms empty File objects to undefined.

// Required file
const required = file(); // or file(v.instance(File))
file().parse(new File([], "empty.txt")); // → ValidationError
file().parse(new File(["data"], "file.txt")); // → File

// Optional file
const optional = file(v.optional(v.instance(File)));
optional.parse(new File([], "empty.txt")); // → undefined

// With validation (using Valibot's built-in validators)
const imageOnly = file(v.pipe(v.instance(File), v.mimeType(["image/png", "image/jpeg"])));
imageOnly.parse(new File(["data"], "file.txt")); // → ValidationError

repeatable(schema?)

Preprocesses fields where multiple values may be present for the same field name. Always returns an array.

const tags = repeatable(); // defaults to array of text()
tags.parse(["a", "b"]); // → ["a", "b"]
tags.parse("single"); // → ["single"]
tags.parse(undefined); // → []

// With minimum length requirement
const atLeastOne = repeatable(v.pipe(v.array(text()), v.minLength(1)));
atLeastOne.parse([]); // → ValidationError
atLeastOne.parse(["item"]); // → ["item"]

repeatableOfType(itemSchema)

Convenience wrapper for repeatable. Pass the schema for individual items instead of the entire array.

const numbers = repeatableOfType(numeric());
numbers.parse(["1", "2", "3"]); // → [1, 2, 3]
numbers.parse("42"); // → [42]
numbers.parse(undefined); // → []

preprocessFormData(formData)

Preprocesses FormData or URLSearchParams (or any iterable of key-value pairs) into a nested object structure. This function is used internally by formData(), but is also exported for cases where you want to preprocess the data separately.

  • Converts single values to the value itself, multiple values to arrays
  • Parses dot notation (address.street) into nested objects
  • Parses bracket notation (items[0][name]) into nested arrays/objects
  • Supports mixed notation
import { preprocessFormData } from "@typed-web/valibot-form-data";

const formData = new FormData();
formData.append("name", "John");
formData.append("address.street", "123 Main St");
formData.append("address.city", "NYC");
formData.append("tags", "tag1");
formData.append("tags", "tag2");

const obj = preprocessFormData(formData);
// → {
//   name: "John",
//   address: { street: "123 Main St", city: "NYC" },
//   tags: ["tag1", "tag2"]
// }

Advanced Examples

Nested Objects

Use v.object() to define nested object structures. The form field names should use dot notation:

const schema = formData({
  name: text(),
  address: v.object({
    street: text(),
    city: text(),
  }),
});

// HTML form:
// <input name="name" value="John" />
// <input name="address.street" value="123 Main St" />
// <input name="address.city" value="Anytown" />

// Result: { name: "John", address: { street: "123 Main St", city: "Anytown" } }

Arrays of Objects

Use v.array() with v.object() for arrays of objects. The form field names can use dot notation with indices or bracket notation:

const schema = formData({
  locations: v.array(
    v.object({
      country: text(),
      city: text(),
    }),
  ),
});

// HTML form (dot notation):
// <input name="locations.0.country" value="USA" />
// <input name="locations.0.city" value="New York" />
// <input name="locations.1.country" value="Canada" />
// <input name="locations.1.city" value="Toronto" />

// Or using bracket notation:
// <input name="locations[0][country]" value="USA" />
// <input name="locations[0][city]" value="New York" />
// <input name="locations[1][country]" value="Canada" />
// <input name="locations[1][city]" value="Toronto" />

// Result: {
//   locations: [
//     { country: "USA", city: "New York" },
//     { country: "Canada", city: "Toronto" }
//   ]
// }

Complex Example

import * as v from "valibot";
import {
  formData,
  text,
  numeric,
  checkbox,
  file,
  repeatableOfType,
} from "@typed-web/valibot-form-data";

const userSchema = formData({
  // Basic fields
  name: text(),
  email: text(v.pipe(v.string(), v.email())),
  age: numeric(v.pipe(v.number(), v.minValue(18))),

  // Optional fields
  website: text(v.optional(v.pipe(v.string(), v.url()))),
  bio: text(v.optional(v.string())),

  // Checkbox
  acceptTerms: checkbox(),

  // File upload
  avatar: file(v.optional(v.instance(File))),

  // Nested object (form fields use dot notation: address.street, address.city, address.zip)
  address: v.object({
    street: text(),
    city: text(),
    zip: text(v.pipe(v.string(), v.regex(/^\d{5}$/))),
  }),

  // Multiple values (form fields: hobbies[], scores[])
  hobbies: repeatableOfType(text()),
  scores: repeatableOfType(numeric()),
});

// Usage with HTML form
const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
  e.preventDefault();
  const formData = new FormData(form);

  try {
    const data = v.parse(userSchema, formData);
    console.log(data);
    // {
    //   name: "John Doe",
    //   email: "[email protected]",
    //   age: 25,
    //   website: "https://example.com",
    //   bio: undefined,
    //   acceptTerms: true,
    //   avatar: File { ... },
    //   address: {
    //     street: "123 Main St",
    //     city: "New York",
    //     zip: "10001"
    //   },
    //   hobbies: ["reading", "gaming"],
    //   scores: [95, 87, 92]
    // }
  } catch (error) {
    console.error("Validation failed:", error);
  }
});

Usage with React Router / Remix

This library works great with React Router's Form component and Remix's action handlers:

// app/routes/users.new.tsx
import { Form } from "react-router";
import type { Route } from "./+types/users.new";
import * as v from "valibot";
import { formData, text, numeric } from "@typed-web/valibot-form-data";

const userSchema = formData({
  name: text(),
  email: text(v.pipe(v.string(), v.email())),
  age: numeric(v.pipe(v.number(), v.minValue(18))),
});

export async function action({ request }: Route.ActionArgs) {
  const data = await request.formData();

  try {
    const user = v.parse(userSchema, data);
    // Save user to database
    return { success: true, user };
  } catch (error) {
    return { success: false, errors: error.issues };
  }
}

export default function NewUser() {
  return (
    <Form method="post">
      <input type="text" name="name" />
      <input type="email" name="email" />
      <input type="number" name="age" />
      <button type="submit">Submit</button>
    </Form>
  );
}

Comparison with zod-form-data

If you're familiar with zod-form-data, here's a quick comparison:

| zod-form-data | @typed-web/valibot-form-data | | ------------------------ | ---------------------------- | | zfd.text() | text() | | zfd.numeric() | numeric() | | zfd.checkbox() | checkbox() | | zfd.file() | file() | | zfd.repeatable() | repeatable() | | zfd.repeatableOfType() | repeatableOfType() | | zfd.formData({ ... }) | formData({ ... }) | | Based on Zod | Based on Valibot |

The API is intentionally similar to make migration easier.

License

MIT - See LICENSE