Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ This will result in the following JSON structure:
}
```

### Repeated field names

A field name repeated without brackets, such as a checkbox group, also becomes an array.

```html
<form>
<input type="checkbox" name="interests" value="music" checked />
<input type="checkbox" name="interests" value="sports" checked />
</form>
```

```json
{
"interests": ["music", "sports"]
}
```

### Complex structure

You can mix dot and square bracket notation to create complex structures.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@formspark/formson",
"description": "FormData to JSON converter",
"private": false,
"version": "0.2.1",
"version": "0.3.0",
"author": "Bjorn Krols (https://formspark.io)",
"license": "MIT",
"keywords": [
Expand Down
28 changes: 24 additions & 4 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,31 @@ describe("formson", () => {
expect(toJSON(formData)).toEqual({ note: "" });
});

test("repeated key keeps the last value", () => {
test("repeated key becomes an array", () => {
const formData = new FormData();
formData.append("tag", "first");
formData.append("tag", "second");
expect(toJSON(formData)).toEqual({ tag: "second" });
formData.append("tag", "third");
expect(toJSON(formData)).toEqual({ tag: ["first", "second", "third"] });
});

test("repeated key at a nested path becomes an array", () => {
const formData = new FormData();
formData.append("person.tags", "first");
formData.append("person.tags", "second");
expect(toJSON(formData)).toEqual({
person: { tags: ["first", "second"] },
});
});

test("repeated File values become an array", () => {
const a = new File(["a"], "a.txt", { type: "text/plain" });
const b = new File(["b"], "b.txt", { type: "text/plain" });
const formData = new FormData();
formData.append("uploads", a);
formData.append("uploads", b);
const json = toJSON(formData);
expect(json.uploads).toEqual([a, b]);
});

test("File values pass through unchanged", () => {
Expand Down Expand Up @@ -203,11 +223,11 @@ describe("formson", () => {
expect(toJSON(formData)).toEqual({ items: { "a.b": "x" } });
});

test("empty brackets collapse to a scalar key", () => {
test("empty brackets become an array, same as a repeated plain key", () => {
const formData = new FormData();
formData.append("items[]", "a");
formData.append("items[]", "b");
expect(toJSON(formData)).toEqual({ items: "b" });
expect(toJSON(formData)).toEqual({ items: ["a", "b"] });
});

test("ignores prototype-pollution keys", () => {
Expand Down
15 changes: 14 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,20 @@ export const toJSON = function (formData: FormData): Record<string, any> {
nextIndex <= MAX_ARRAY_INDEX;

if (isLast) {
current[part] = value;
const existing = current[part];
if (Array.isArray(existing)) {
existing.push(value);
} else if (
existing !== undefined &&
!(typeof existing === "object" && existing.constructor === Object)
) {
// A second plain occurrence of the same key (e.g. a checkbox
// group): promote to an array instead of overwriting, matching
// how repeated form fields are parsed server-side.
current[part] = [existing, value];
} else {
current[part] = value;
}
} else {
if (isNextArray) {
if (!Array.isArray(current[part])) {
Expand Down