diff --git a/README.md b/README.md index e523f14..d4cee44 100644 --- a/README.md +++ b/README.md @@ -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 +
+ + +
+``` + +```json +{ + "interests": ["music", "sports"] +} +``` + ### Complex structure You can mix dot and square bracket notation to create complex structures. diff --git a/package.json b/package.json index a341c7c..bd51222 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/src/index.test.ts b/src/index.test.ts index 9401085..5cbd997 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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", () => { @@ -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", () => { diff --git a/src/index.ts b/src/index.ts index c8d45d3..ddbe2fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,7 +30,20 @@ export const toJSON = function (formData: FormData): Record { 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])) {