form-codec
v1.0.2
Published
FormData round-trips your objects — Server Actions lose your types back to strings, this doesn't.
Maintainers
Readme
form-codec
FormData round-trips your objects — Server Actions lose your types back to strings, this doesn't.
Convert JavaScript objects to FormData and back, perfectly reconstructing booleans, numbers, dates, nulls, arrays, and deeply nested structures without losing type fidelity.
Built specifically for React 19 / Next.js / Remix Server Actions where Object.fromEntries(formData) silently corrupts non-string types.
Why form-codec?
Browsers and modern frameworks (like Next.js Server Actions, Remix, and React 19 Actions) natively turn everything in a form into a string. A checkbox becomes "true", a number becomes "25", and a date becomes a raw ISO string, leaving you with FormData losing types in your Next.js Server Action. form-codec fixes this by remembering the original types, so decode() gives back real booleans, numbers, and dates — not strings you have to manually convert yourself. It is the only package that does both directions (object → FormData and FormData → object) with full type fidelity, whereas alternatives only go one way.
Quick Start
Step 1: Install
npm install form-codecStep 2: Import
import { encode, decode } from "form-codec";Step 3: Use
const original = { name: "Alice", active: true, age: 30 };
const formData = encode(original);
// ... transmit formData to your server ...
const restored = decode(formData);
console.log(restored.active); // true (boolean, not "true"!)
console.log(restored.age); // 30 (number, not "30"!)1. The Type-Loss Bug
Server Actions naturally receive raw FormData, which only supports strings and File objects.
The broken way (native):
// ❌ Server Actions lose your types
const data = { active: true, age: 25, date: new Date() };
// 1. You submit it via Server Action or fetch...
// 2. The server reconstructs it:
const broken = Object.fromEntries(formData);
// { active: "true", age: "25", date: "2024-01-01T..." }
// 🚨 Booleans are strings, Numbers are strings, Dates are strings, Arrays are missing!The form-codec way:
import { encode, decode } from "form-codec";
// ✅ Types are preserved perfectly
const fd = encode({ active: true, age: 25, date: new Date() });
const fixed = decode(fd);
// { active: true, age: 25, date: Date }2. Why it exists
While existing tools like object-to-formdata excel at one-way serialization, they offer no standard way to deserialize back to the exact same types. form-codec embeds a tiny, hidden type-marker (__form-codec) into the FormData during serialization, allowing the server to losslessly reconstruct the original object tree with zero guessing or manual parsing.
3. Install
npm install form-codecZero dependencies. ESM & CommonJS supported.
4. Basic Usage
import { encode, decode } from "form-codec";
// Client-side
const formData = encode({ name: "Alice", active: true });
await myServerAction(formData);
// Server-side
async function myServerAction(formData: FormData) {
const data = decode(formData);
// data.active === true
}5. Nested Object Example
const user = {
profile: {
city: "New York",
zip: 10001
}
};
const fd = encode(user);
// fd.get('profile[city]') === "New York"
// fd.get('profile[zip]') === "10001"
const decoded = decode(fd); // { profile: { city: "New York", zip: 10001 } }6. Array Example
const form = encode({ tags: ["react", "nextjs"] });
// fd.getAll('tags[]') === ["react", "nextjs"]
const decoded = decode(form); // { tags: ["react", "nextjs"] }7. File Upload Example
const file = new File(["content"], "document.txt", { type: "text/plain" });
const fd = encode({
title: "My Upload",
documents: [file]
});
const decoded = decode(fd);
// decoded.documents[0] is a File object!8. Configuration
Both encode and decode accept an options object.
const fd = encode(data, {
arrayFormat: "brackets", // "brackets" | "indexed" | "comma"
objectFormat: "brackets", // "brackets" | "dot"
nullHandling: "skip", // "skip" | "empty" | "stringify"
undefinedHandling: "skip", // "skip" | "empty"
maxDepth: 32,
include: ["name", "email"],
exclude: ["internal_id"],
rename: { "name": "fullName" },
transform: {
"age": (val) => Number(val) + 1
}
});9. Serialization Strategies
By default, form-codec uses brackets notation (tags[] and profile[city]), which is the universally accepted standard for FormData arrays and nested properties in PHP, Ruby on Rails, and Express.
You can switch to dot-notation or indexed arrays:
encode(data, { objectFormat: "dot", arrayFormat: "indexed" });
// Result: profile.city="New York", tags[0]="react", tags[1]="nextjs"10. Framework Entry Points
form-codec ships with subpath exports specifically tailored for bundler tree-shaking and SEO. For now, they mirror the core API, but will expand with framework-specific wrappers:
import { encode, decode } from "form-codec/next"; // Next.js Server Actions
import { encode, decode } from "form-codec/react"; // React 19 Actions (useActionState)
import { encode, decode } from "form-codec/remix"; // Remix Actions11. TypeScript Usage
form-codec is written in TypeScript and provides generic types for fully typed decoding:
type User = { id: number; name: string };
const user = decode<User>(formData);
// user.id is correctly inferred as number12. Browser and Node Compatibility
Works flawlessly in the Browser and Node.js 18+ (which includes native FormData, File, and Blob).
13. Error Handling
form-codec fails safely:
- Circular References: Throws an error immediately to prevent infinite recursion, unlike naive stringifiers.
- Max Depth: Prevents stack overflows on malicious deep payloads (default depth: 32).
14. Performance
Fast and memory-efficient. form-codec iterates over objects in a single pass without large intermediate ASTs.
15. Known Limitations
Empty Collections Are Skipped
Empty arrays ([]) and empty objects ({}) are skipped during encoding because FormData only encodes key-value pairs. During decoding, they are indistinguishable from a field that was never set.
Null/Undefined Behavior with "skip"
By default, null and undefined values are skipped (nullHandling: "skip"). When skipped, they are omitted from the FormData. When decoded, these properties will simply not exist on the reconstructed object, which typically evaluates to undefined. If you want to accurately reconstruct null versus undefined, you must explicitly configure { nullHandling: "empty", undefinedHandling: "empty" } (or "stringify" for nulls).
Key-Collision with Format Syntaxform-codec parses string keys containing brackets (tags[]) and dots (profile.name) into nested paths. If you have an object with a literal key name that mimics this syntax (e.g., { "a.b": "value" } or { "tags[]": "value" }), it cannot be distinguished from a nested path during decoding, and will be reconstructed as { a: { b: "value" } }. Do not use literal periods or brackets in your root keys.
16. API Reference
encode(data: Record<string, any>, options?: EncodeOptions): FormData
Converts a JavaScript object to a FormData instance, appending a hidden __form-codec type marker.
decode<T>(formData: FormData, options?: DecodeOptions): T
Reconstructs a JavaScript object from a FormData instance using the embedded type marker.
17. FAQ
Q: Does it support BigInt?
Yes, BigInt is fully supported and properly decoded.
Q: Can I use this without the type marker?
Yes, if __form-codec is missing, it will decode nested structures safely, though primitives will remain strings unless you set coerce: true in decode options.
18. Contributing
Contributions are welcome! Please open an issue before submitting a large PR.
License
MIT ©
