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 🙏

© 2025 – Pkg Stats / Ryan Hefner

formstruct

v1.0.3

Published

Effortlessly transform raw form data into structured objects using JSON Schema.

Readme

FormStruct

A lightweight TypeScript library for parsing HTML form data into structured objects based on JSON Schema types. Designed to handle complex form inputs including multiselect, checkbox groups, and nullable fields.

📚 Table of Contents

✨ Features

  • Parse FormData into structured JavaScript objects
  • Support for complex form inputs (multiselect, checkbox groups)
  • Handle nested objects and arrays with dot notation
  • High performance with schema preprocessing
  • Support for nullable fields and default values
  • Clean and type-safe API

🚀 Installation

npm install formstruct
# or
yarn add formstruct
# or
pnpm add formstruct

🎯 Basic Usage

import { createParser } from "formstruct";

// Define your schema
const schema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "number" },
    email: {
      anyOf: [{ type: "string", format: "email" }, { type: "null" }],
    },
    preferences: {
      type: "object",
      properties: {
        newsletter: { type: "boolean" },
        theme: { type: "string", enum: ["light", "dark"] },
      },
      required: ["newsletter", "theme"],
    },
    books: {
      type: "array",
      items: {
        type: "object",
        properties: {
          title: { type: "string" },
          author: { type: "string" },
        },
        required: ["title", "author"],
      },
    },
    languages: { type: "array", items: { type: "string" } },
  },
  required: ["name", "age", "email", "preferences", "books", "languages"],
};

// Create parser
const parser = createParser(schema);

// Parse form data
const formData = new FormData();
formData.append("name", "John Doe");
formData.append("age", "30");
formData.append("email", "[email protected]");
formData.append("preferences.newsletter", "true");
formData.append("preferences.theme", "dark");
formData.append("books[0].title", "Clean Code");
formData.append("books[0].author", "Robert Martin");
formData.append("books[1].title", "TypeScript Handbook");
formData.append("books[1].author", "Microsoft");
formData.append("languages", "typescript");
formData.append("languages", "rust");

const result = parser(formData);
/* Result:
{
  name: "John Doe",
  age: 30,
  email: "[email protected]",
  preferences: {
    newsletter: true,
    theme: "dark"
  },
  books: [
    {
      title: "Clean Code",
      author: "Robert Martin"
    },
    {
      title: "TypeScript Handbook",
      author: "Microsoft"
    }
  ],
  languages: ["typescript", "rust"]
}
*/

📝 Complete Form Example

<form id="userProfileForm">
  <!-- Basic Fields -->
  <input type="text" name="name" required />
  <input type="number" name="age" required />

  <!-- Nullable Email -->
  <input type="email" name="email" />
  <!-- Can be empty -->

  <!-- Preferences (Required Object) -->
  <div>
    <input type="checkbox" name="preferences.newsletter" required />
    <select name="preferences.theme" required>
      <option value="light">Light</option>
      <option value="dark">Dark</option>
    </select>
  </div>

  <!-- Books Array (Required) -->
  <div>
    <input type="text" name="books[0].title" required />
    <input type="text" name="books[0].author" required />
    <!-- Add more book fields dynamically -->
  </div>

  <!-- Languages (Multiple Select) -->
  <select name="languages" multiple required>
    <option value="javascript">JavaScript</option>
    <option value="typescript">TypeScript</option>
    <option value="python">Python</option>
    <option value="rust">Rust</option>
  </select>
</form>

🔧 Form Input Types

Basic Fields

<input type="text" name="name" required />
<input type="number" name="age" required />

Nullable Fields

<input type="email" name="email" />
<!-- Empty string will be parsed as null if schema allows -->

Nested Objects

<input type="checkbox" name="preferences.newsletter" required />
<select name="preferences.theme" required>
  <option value="light">Light</option>
  <option value="dark">Dark</option>
</select>

Multiple Select

<select name="languages" multiple required>
  <option value="typescript">TypeScript</option>
  <option value="rust">Rust</option>
</select>

🔌 Schema Validator Integration

FormStruct can be used with popular schema validators by converting their schemas to JSON Schema. Here's how to use it with different validators:

Using with Zod

import { z } from "zod";
import { createParser } from "formstruct";
import { zodToJsonSchema } from "zod-to-json-schema";

const userSchema = z.object({
  name: z.string(),
  age: z.number(),
  email: z.string().email().nullable(),
  preferences: z.object({
    newsletter: z.boolean(),
    theme: z.enum(["light", "dark"]),
  }),
  books: z.array(
    z.object({
      title: z.string(),
      year: z.number(),
    })
  ),
  languages: z.array(z.string()),
});

// Create type-safe parser
const parser = createParser<z.infer<typeof userSchema>>(
  zodToJsonSchema(userSchema)
);

// Parse form data
const result = parser(formData);

💡 Tip: Create a reusable Zod adapter:

import type { z } from "zod";
import { createParser } from "formstruct";
import { zodToJsonSchema } from "zod-to-json-schema";

export const adapter = <T extends z.ZodType>(schema: T) => {
  return createParser<z.infer<T>>(zodToJsonSchema(schema));
};

// Usage
const parser = adapter(userSchema);
const result = parser(formData);

Using with Valibot

import * as v from "valibot";
import { createParser } from "formstruct";
import { toJsonSchema } from "@valibot/to-json-schema";

const userSchema = v.object({
  name: v.string(),
  age: v.number(),
  email: v.nullable(v.pipe(v.string(), v.email())),
  preferences: v.object({
    newsletter: v.boolean(),
    theme: v.union([v.literal("light"), v.literal("dark")]),
  }),
  books: v.array(
    v.object({
      title: v.string(),
      year: v.number(),
    })
  ),
  languages: v.array(v.string()),
});

// Create type-safe parser
const parser = createParser<v.Output<typeof userSchema>>(
  toJsonSchema(userSchema)
);

// Parse form data
const result = parser(formData);

💡 Tip: Create a reusable Valibot adapter:

import type { BaseSchema, Output } from "valibot";
import { createParser } from "formstruct";
import { toJsonSchema } from "@valibot/to-json-schema";

export const adapter = <T extends BaseSchema>(schema: T) => {
  return createParser<Output<T>>(toJsonSchema(schema));
};

// Usage
const parser = adapter(userSchema);
const result = parser(formData);

Using with Yup

import * as yup from "yup";
import { createParser } from "formstruct";
import { convertSchema } from "@sodaru/yup-to-json-schema";

const userSchema = yup.object({
  name: yup.string().required(),
  age: yup.number().required(),
  email: yup.string().email().nullable(),
  preferences: yup
    .object({
      newsletter: yup.boolean().required(),
      theme: yup.string().oneOf(["light", "dark"]).required(),
    })
    .required(),
  books: yup
    .array()
    .of(
      yup.object({
        title: yup.string().required(),
        year: yup.number().required(),
      })
    )
    .required(),
  languages: yup.array().of(yup.string()).required(),
});

// Create type-safe parser
const parser = createParser<yup.InferType<typeof userSchema>>(
  convertSchema(userSchema)
);

// Parse form data
const result = parser(formData);

💡 Tip: Create a reusable Yup adapter:

import type { Schema, InferType } from "yup";
import { createParser } from "formstruct";
import { convertSchema } from "@sodaru/yup-to-json-schema";

export const adapter = <T extends Schema>(schema: T) => {
  return createParser<InferType<T>>(convertSchema(schema));
};

// Usage
const parser = adapter(userSchema);
const result = parser(formData);

🛡️ Type Safety

All schema validators provide full type inference, giving you:

  • Type checking for form data structure
  • Autocomplete for object properties
  • Type errors if you try to access non-existent properties
  • Proper types for nullable fields and enums

📖 API Reference

createParser(schema: JSONSchema7)

Creates a parser function based on the provided JSON Schema.

Parameters

  • schema: A valid JSON Schema (version 7) that describes the expected structure of your data.

Returns

  • A parser function that takes FormData as input and returns a structured object.

Form Data Naming Convention

  • Use dot notation for nested objects: preferences.theme
  • Use array notation for arrays: books[0].title
  • Multiple values for the same name become arrays: languages

❓ Troubleshooting

Common Issues

  1. Form Data Not Parsing

    // ❌ Wrong
    formData.append("preferences", JSON.stringify({ theme: "dark" }));
    
    // ✅ Correct
    formData.append("preferences.theme", "dark");
  2. Array Handling

    // ❌ Wrong
    formData.append("books", JSON.stringify([{ title: "Book" }]));
    
    // ✅ Correct
    formData.append("books[0].title", "Book");
  3. Nullable Fields

    // ❌ Wrong: Schema doesn't allow null
    const schema = { email: { type: "string" } };
    
    // ✅ Correct: Schema allows null
    const schema = {
      email: { anyOf: [{ type: "string" }, { type: "null" }] },
    };
  4. Type Coercion

    // ❌ Wrong: String won't be coerced to number
    const schema = { age: { type: "string" } };
    
    // ✅ Correct: String will be coerced to number
    const schema = { age: { type: "number" } };

🚫 Limitations

FormStruct is designed to be a lightweight form data parser, not a full schema validator. Here are some important limitations to be aware of:

Compound Schema Types (anyOf/oneOf/allOf)

  • For primitive types (string, number, boolean), the parser will use the first schema from anyOf/oneOf/allOf
  • For object/array types, it attempts to match the first schema based on property names
  • If no matching schema is found for an object/array, the field will not be transformed

Object Initialization

  • Child objects are only initialized when at least one of their properties is present in the form data
  • Default values in nested objects won't trigger object initialization if no form data is provided for that object path
  • Nested default values are only applied when their parent object is initialized by form data

Validation

  • FormStruct does not perform schema validation
  • It only handles data transformation according to the schema types

💡 Tip: For conditional validation, transform the data before passing it to a validator (FormData → FormStruct → Transform → Validate)

Type Coercion

  • Basic type coercion is performed (string to number/boolean)
  • Complex type coercion (e.g., string to date) is not supported
  • Custom formats are not validated

🔄 Version Compatibility

| FormStruct | Node.js | TypeScript | | ---------- | -------- | ---------- | | 1.x | ≥ 12.0.0 | ≥ 4.5.0 |

Browser Support

Supports all modern browsers that implement:

  • FormData API (ES2017)
  • Optional chaining and nullish coalescing (ES2020)
  • Array methods (find, push)
  • Template literals

Minimum versions:

  • Chrome ≥ 85
  • Firefox ≥ 88
  • Safari ≥ 14
  • Edge ≥ 85

For older browsers, you may need a polyfill for:

  • Optional chaining (?.)
  • Nullish coalescing (??)

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

MIT