Open-source project
maxatwork/form2js avatar
maxatwork/form2js

form2js parses browser forms into objects through six focused packages

Parse browser forms into structured JavaScript objects. Six adapters — React hooks, vanilla DOM, jQuery, FormData, and more. One coherent API.

633 stars135 forksTypeScriptMIT

At a glance

What is it?
form2js is a MIT licensed family of six npm packages that turn form-shaped data into plain objects and objects back into form fields, with separate adapters for React hooks, the DOM, jQuery, FormData and Node pipelines, plus a shared core for path parsing. It was originally written in 2010 and rewritten for TypeScript and ESM, and the current packages are published at version 3.4.1 with the last push on 2026-10-10.
Who is it for?
Adopt form2js when your forms already use dotted field names and bracket suffixes, and you want the same parsing rules in the browser, in React and in a Node pipeline without writing each one yourself. Do not adopt it for a single form with a handful of fields, where a short loop over FormData entries does the job, and note that the naming convention is a commitment your markup has to keep.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 13 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One parsing model, six environment specific packages

form2js does one job: turning form-shaped data into objects and objects back into forms. The README is unusually blunt about the scope, saying it is not a serializer, not an ORM and not a new religion, that it does this one job reliably and leaves before anyone starts a committee about it.

What changed recently is the packaging. The project was originally created in 2010 and has now been rewritten for modern JavaScript, TypeScript, ESM, React and modular usage, with the old single-package version kept on a legacy branch. The rewrite keeps the naming rules and the core parsing model but splits the browser-era API into packages that each suit one environment.

The six packages divide along the axes of where the data comes from and where it goes. @form2js/core is the path parsing and object transformation engine, the piece everything else builds on. @form2js/dom extracts DOM fields into an object through formToObject and the older form2js name, and it ships a standalone build. @form2js/form-data converts FormData or entries into an object and runs on Node. @form2js/js2form is the inverse direction, populating DOM fields from an object through objectToForm and js2form. @form2js/jquery provides the jQuery plugin adapter that restores the $.fn.toObject method. @form2js/react is a React submit hook with parsing and validation state.

The table in the README also records which packages work where, and that is a useful practical detail. The dom and js2form packages need a DOM shim such as jsdom to run under Node, form-data is the one that runs natively on Node, and only dom and jquery ship standalone browser builds.

The project is MIT licensed and lives in a Turborepo monorepo with npm workspaces, requiring Node 22.12.0 or newer, and the packages are published together at version 3.4.1.

Install only the package your environment needs

The installation section is a list of independent commands rather than one meta package, and the point is that you take what you use:

bash
npm install @form2js/react react
npm install @form2js/dom
npm install @form2js/form-data
npm install @form2js/js2form
npm install @form2js/core
npm install @form2js/jquery jquery

For browser use without a bundler, the dom and jquery packages ship a standalone script build at dist/standalone.global.js, and the README lists the same filename for both. That build is loaded as a plain script tag:

html
<script src="https://unpkg.com/@form2js/dom/dist/standalone.global.js"></script>
<script>
  const result = formToObject(document.getElementById("profileForm"));
  // or form2js(...)
</script>

Note what the standalone build gives you. The global form2js name is still available as an alternative to the named export, which matters if you are porting code written against the 2010 package.

The React and jQuery packages pull in their peer, react and jquery respectively, while the core and form-data packages are dependency free in the examples the README shows. The npm manifest of the monorepo sets an engines field requiring Node 22.12.0 or later and pins npm as the package manager at version 11.6.2, with Turbo running the build, lint and typecheck tasks across workspaces.

Dotted names and bracket suffixes become nested objects

The naming rules are the heart of the library, and they are the same rules the legacy version used. A dotted field name builds nesting, and a trailing empty bracket pair builds an array.

The example form in the README makes it concrete:

html
<form id="profileForm">
  <input name="person.name.first" value="Esme" />
  <input name="person.name.last" value="Weatherwax" />
  <label
    ><input type="checkbox" name="person.tags[]" value="witch" checked />
    witch</label
  >
</form>

Reading that markup with the dom adapter gives a nested object:

ts
import { formToObject } from "@form2js/dom";

const result = formToObject(document.getElementById("profileForm"));
// => { person: { name: { first: "Esme", last: "Weatherwax" }, tags: ["witch"] } }

So person.name.first becomes a string three levels deep, and the repeated person.tags[] checkbox becomes a one element array. The convention is carried over from the legacy version, which the README insists on, and it is the kind of decision that quietly shapes your markup for years. Choosing this library means committing to dotted names in your HTML.

That is also where the real difference from a hand written loop appears. A ten line function over FormData entries can produce a flat object, but producing the nested shape above with correct array semantics takes a path parser, which is what @form2js/core exists to be.

The inverse direction exists for populating a form from an object, and the jquery adapter offers the same parsing behind the familiar toObject method, with a mode option where the README's example passes mode: "first".

The same engine runs over FormData in a Node pipeline

Server-side and pipeline use is handled by @form2js/form-data, which converts FormData or entries into an object. The README notes that Node 18 and later have a global FormData, so the same call works on the server.

For pipelines where a FormData instance is not available, the package also accepts iterable entries directly, which the README calls out as handy in server pipelines:

ts
import { entriesToObject } from "@form2js/form-data";

const result = entriesToObject([
  ["person.name.first", "Sam"],
  ["person.roles[]", "captain"],
]);
// => { person: { name: { first: "Sam" }, roles: ["captain"] } }

This is the part of the package that makes it more than a browser helper. The same object shape is produced in the browser, on the server and inside a queue consumer, which means a form post can be validated in one place with one set of rules instead of two implementations that drift apart.

The Node story is also the reason the core package exists separately. Path parsing has no browser dependency, so @form2js/core carries the rules and the adapters wrap them.

One asymmetry is worth knowing. The README states that @form2js/form-data is not shipped as a standalone build, so module imports are the only route for that package, unlike dom and jquery which have script builds for pages that do not use a bundler.

Schema validation is passed into the parse call

Parsing a form gives you strings, which is the awkward part of every form library. form2js addresses it by accepting a schema object at the call site, and the README says it works with Zod or any schema exposing a parse method that takes an unknown value.

The example uses Zod, and the interesting part is the coercion:

ts
import { z } from "zod";
import { formDataToObject } from "@form2js/form-data";

const PersonSchema = z.object({
  person: z.object({
    age: z.coerce.number().int().min(0)
  })
});

const result = formDataToObject([["person.age", "42"]], {
  schema: PersonSchema
});
// => { person: { age: 42 } }

The input was the string 42 and the output is the number 42, because the schema asks for a coerced number and the parse happens inside the form data call. This is a compact design: the schema is not a separate validation step you have to remember to call, it is an option on the parse, so it is impossible to parse without validating when you asked for it.

Because the requirement is only a parse method, the library is not tied to Zod. Any validation library whose schema object can parse an unknown value works, which matters if your project already standardised on a different one.

The React package takes the same idea and adds the state around it, so a form can report a validation error without the component wiring its own error handling.

The React package turns parsing into a submit hook

@form2js/react is described as a React submit hook with parsing and validation state, and its example returns a set of values from useForm2js: onSubmit, isSubmitting, isError, error, isSuccess and reset.

The pattern in the README's example is worth noting. The hook is called with an async handler that receives the parsed and validated data, and with an options object carrying the schema. When a schema is provided, the data type is inferred, which means the handler's parameter is typed from the schema rather than declared by hand.

The component then wires the hook's onSubmit to the form's own submit event, renders a disabled button while isSubmitting is true, renders the error and success states, and offers a reset button for the state. The library's role is the part between the submit event and your handler: reading the form, parsing it, validating it against the schema and handing you a typed object, with the async lifecycle state included.

The reset in that list is a state reset rather than a field clear, since the form is uncontrolled and the input keeps its defaultValue. The js2form package is the tool for the other direction, writing an object into the fields of a form.

The practical advantage over wiring this by hand is consistency: the same naming rules and the same schema path are used, so a form handled in React and a form handled on the server produce the same object.

Migrating from the 2010 package is a package map

For anyone with existing code, the README gives a quick package map. Legacy browser form2js(...) calls move to @form2js/dom, and the jQuery $("#form").toObject() style moves to @form2js/jquery. Server or pipeline FormData parsing moves to @form2js/form-data, React submit handling moves to @form2js/react, and turning an object back into fields moves to @form2js/js2form. A migration guide is published alongside the documentation site.

The repository is a monorepo with packages/, apps/, docs/, test/, scripts/ and a changeset/ directory, and the tooling around it is worth a look. Turbo runs build, lint, typecheck and test across workspaces, Vitest runs the integration tests in test/integration, and the docs app has its own end to end suite that runs against a built package output on a dedicated port. Release versions are managed with changesets, and there are separate scripts for rewriting the package scope and for bumping the version across every package.

That release tooling explains something visible in the repository's release history. The three most recent tags are @form2js/react at 3.4.1, @form2js/js2form at 3.4.1 and @form2js/jquery at 3.4.1, all published on 2026-08-05, which is what a changesets managed monorepo looks like when packages move together.

The limitation to keep in mind is the one the naming rules impose. This is a tool for forms whose field names follow a convention, and if your markup uses flat names like user_email, the output will be a flat object and the nesting you wanted will not appear. Nothing in the library invents structure your names do not carry.

Editorial conclusion

Adopt form2js when your forms already use dotted field names and bracket suffixes, and you want the same parsing rules in the browser, in React and in a Node pipeline without writing each one yourself. Do not adopt it for a single form with a handful of fields, where a short loop over FormData entries does the job, and note that the naming convention is a commitment your markup has to keep. Verify first by running the @form2js/dom adapter against one production form and comparing its output with what your server currently receives, because the conventions differ from URL encoding and from multipart field names.

Frequently asked questions

How do I install form2js?

Install only the packages you need, for example npm install @form2js/dom for browser form parsing, npm install @form2js/form-data for FormData or entries parsing, and npm install @form2js/react react for the React hook. The dom and jquery packages also ship a standalone browser build at dist/standalone.global.js.

How does form2js turn form field names into nested objects?

Dotted names build nesting and a trailing bracket pair builds an array, so person.name.first becomes a nested string and person.tags[] becomes an array of values. The README notes the current packages keep the naming rules and core parsing model of the legacy version.

Can form2js validate parsed form data with Zod?

Yes. The form-data package accepts a schema option at the call site and the README says this works with Zod or any schema exposing a parse method for an unknown value, so a coerced numeric field can arrive as a number instead of a string.

Official sources

  1. License: MIT
  2. maxatwork/form2js on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/maxatwork-form2js.svg)](https://hysenlabs.com/projects/maxatwork-form2js)

Was this useful?