Before you can POST a form as JSON or drop it into state, you have to turn its scattered fields into one object. That sounds trivial until you hit the real rules: unchecked checkboxes vanish, a checkbox group becomes an array, a multi-select yields several values under one name, and names like user[address][city] are meant to build nested structure. This is exactly what jQuery.serializeObject, the qs library, and Rails' form params do.
Implement formSerialize(form) returning a nested object. Lean on FormData, which already knows which controls are "successful". See MDN: FormData.
function formSerialize(form) {
// returns a plain (possibly nested) object
}
// <input name="user[name]" value="Ada">
// <input type="checkbox" name="tags[]" value="a" checked>
// <input type="checkbox" name="tags[]" value="b" checked>
formSerialize(form);
// { user: { name: 'Ada' }, tags: ['a', 'b'] }
// <input type="checkbox" name="agree"> (unchecked)
// <select name="colors" multiple> red*, blue* selected
formSerialize(form);
// { colors: ['red', 'blue'] } — no `agree` key at all
new FormData(form) applies these rules for you.user[addr][city] → { user: { addr: { city } } }; tags[] appends into an array.name[]) collects multiple values into one array.value is 'on'.We'll let FormData gather the successful controls, parse each field name into a key path, and write each value into a nested object — arrayifying where the name (or a repeat) calls for it.
Two hard parts hide behind "serialize a form". First, which fields count: unchecked boxes and unselected radios shouldn't appear, and a multi-select contributes several values. FormData already encodes those rules, so we start there. Second, shape: a name like user[addr][city] isn't a flat key — it's a path into nested objects, and tags[] (or a repeated name) means "collect into an array".
Each FormData entry is a (name, value) pair. Turn the name into a list of keys — user[addr][city] → ['user','addr','city'], tags[] → ['tags',''] — then walk the object creating a child per key and setting the value at the end. An empty key ('') or a name that repeats means "push into an array".
The one-liner loses every nuance:
function serializeNaive(form) {
return Object.fromEntries(new FormData(form));
}
FormData gets the successful-controls part right, but Object.fromEntries overwrites duplicates — a multi-select or checkbox group keeps only its last value — and it treats user[name] as a single literal key, so you get { 'user[name]': 'Ada' } instead of nesting. We need to parse the names and merge repeats, not just dump entries.
function formSerialize(form) {
const result = {};
for (const [name, value] of new FormData(form)) {
assign(result, parseName(name), value);
}
return result;
}
// 'user[addr][city]' -> ['user','addr','city']; 'tags[]' -> ['tags','']; 'x' -> ['x']
function parseName(name) {
const base = name.match(/^[^[]+/)[0];
const keys = [base];
const bracket = /\[([^\]]*)\]/g;
let m;
while ((m = bracket.exec(name))) keys.push(m[1]); // '' for a bare []
return keys;
}
function assign(obj, keys, value) {
let cur = obj;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const isLast = i === keys.length - 1;
if (isLast) {
setLeaf(cur, key, value);
} else {
const childIsArray = keys[i + 1] === '';
if (cur[key] == null) cur[key] = childIsArray ? [] : {};
cur = cur[key];
}
}
}
function setLeaf(cur, key, value) {
if (key === '') {
cur.push(value); // tags[] -> append
} else if (key in cur) {
// repeated plain name -> promote to / extend an array
if (Array.isArray(cur[key])) cur[key].push(value);
else cur[key] = [cur[key], value];
} else {
cur[key] = value;
}
}
module.exports = { formSerialize };
Three pieces carry the logic. parseName splits user[addr][city] into a key path (bracket contents, '' for []). assign walks that path, creating an object or array child depending on whether the next key is ''. setLeaf decides the final write: append for [], promote-to-array for a repeated name, otherwise set. Starting from FormData means the successful-control filtering is already done.
A form with user[name]="Ada", tags[]="a" (checked), tags[]="b" (checked), tags[]="c" (unchecked):
FormData yields ['user[name]','Ada'], ['tags[]','a'], ['tags[]','b'] — the unchecked c is dropped.user[name] → keys ['user','name']. assign: user isn't last, next key name (not '') → result.user = {}; then setLeaf(result.user, 'name', 'Ada') → { user: { name: 'Ada' } }.tags[] → keys ['tags','']. assign: tags isn't last, next key '' → result.tags = []; setLeaf([], '', 'a') → push → ['a'].tags[] → result.tags already []; push 'b' → ['a','b'].{ user: { name: 'Ada' }, tags: ['a','b'] }.Object.fromEntries — silently drops all-but-last for duplicate names; multi-selects and checkbox groups lose data. Merge instead.user[name] as a flat key — you must parse the brackets; otherwise the literal string 'user[name]' becomes the key..value — iterating form.elements yourself means re-implementing successful-control rules (checked, disabled, selected). FormData does it correctly; start there.in vs truthiness for promotion — check key in cur (not cur[key]) so a legitimate first value of ''/0 still promotes correctly on a repeat.items[0][name] / items[][name] (grouping sibling brackets into one element) needs index tracking parseName doesn't do here."5"→5, "true"→true, dates, etc.objectToFormData) flattens a nested object back into bracket names for fetch uploads.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Before you can POST a form as JSON or drop it into state, you have to turn its scattered fields into one object. That sounds trivial until you hit the real rules: unchecked checkboxes vanish, a checkbox group becomes an array, a multi-select yields several values under one name, and names like user[address][city] are meant to build nested structure. This is exactly what jQuery.serializeObject, the qs library, and Rails' form params do.
Implement formSerialize(form) returning a nested object. Lean on FormData, which already knows which controls are "successful". See MDN: FormData.
function formSerialize(form) {
// returns a plain (possibly nested) object
}
// <input name="user[name]" value="Ada">
// <input type="checkbox" name="tags[]" value="a" checked>
// <input type="checkbox" name="tags[]" value="b" checked>
formSerialize(form);
// { user: { name: 'Ada' }, tags: ['a', 'b'] }
// <input type="checkbox" name="agree"> (unchecked)
// <select name="colors" multiple> red*, blue* selected
formSerialize(form);
// { colors: ['red', 'blue'] } — no `agree` key at all
new FormData(form) applies these rules for you.user[addr][city] → { user: { addr: { city } } }; tags[] appends into an array.name[]) collects multiple values into one array.value is 'on'.We'll let FormData gather the successful controls, parse each field name into a key path, and write each value into a nested object — arrayifying where the name (or a repeat) calls for it.
Two hard parts hide behind "serialize a form". First, which fields count: unchecked boxes and unselected radios shouldn't appear, and a multi-select contributes several values. FormData already encodes those rules, so we start there. Second, shape: a name like user[addr][city] isn't a flat key — it's a path into nested objects, and tags[] (or a repeated name) means "collect into an array".
Each FormData entry is a (name, value) pair. Turn the name into a list of keys — user[addr][city] → ['user','addr','city'], tags[] → ['tags',''] — then walk the object creating a child per key and setting the value at the end. An empty key ('') or a name that repeats means "push into an array".
The one-liner loses every nuance:
function serializeNaive(form) {
return Object.fromEntries(new FormData(form));
}
FormData gets the successful-controls part right, but Object.fromEntries overwrites duplicates — a multi-select or checkbox group keeps only its last value — and it treats user[name] as a single literal key, so you get { 'user[name]': 'Ada' } instead of nesting. We need to parse the names and merge repeats, not just dump entries.
function formSerialize(form) {
const result = {};
for (const [name, value] of new FormData(form)) {
assign(result, parseName(name), value);
}
return result;
}
// 'user[addr][city]' -> ['user','addr','city']; 'tags[]' -> ['tags','']; 'x' -> ['x']
function parseName(name) {
const base = name.match(/^[^[]+/)[0];
const keys = [base];
const bracket = /\[([^\]]*)\]/g;
let m;
while ((m = bracket.exec(name))) keys.push(m[1]); // '' for a bare []
return keys;
}
function assign(obj, keys, value) {
let cur = obj;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const isLast = i === keys.length - 1;
if (isLast) {
setLeaf(cur, key, value);
} else {
const childIsArray = keys[i + 1] === '';
if (cur[key] == null) cur[key] = childIsArray ? [] : {};
cur = cur[key];
}
}
}
function setLeaf(cur, key, value) {
if (key === '') {
cur.push(value); // tags[] -> append
} else if (key in cur) {
// repeated plain name -> promote to / extend an array
if (Array.isArray(cur[key])) cur[key].push(value);
else cur[key] = [cur[key], value];
} else {
cur[key] = value;
}
}
module.exports = { formSerialize };
Three pieces carry the logic. parseName splits user[addr][city] into a key path (bracket contents, '' for []). assign walks that path, creating an object or array child depending on whether the next key is ''. setLeaf decides the final write: append for [], promote-to-array for a repeated name, otherwise set. Starting from FormData means the successful-control filtering is already done.
A form with user[name]="Ada", tags[]="a" (checked), tags[]="b" (checked), tags[]="c" (unchecked):
FormData yields ['user[name]','Ada'], ['tags[]','a'], ['tags[]','b'] — the unchecked c is dropped.user[name] → keys ['user','name']. assign: user isn't last, next key name (not '') → result.user = {}; then setLeaf(result.user, 'name', 'Ada') → { user: { name: 'Ada' } }.tags[] → keys ['tags','']. assign: tags isn't last, next key '' → result.tags = []; setLeaf([], '', 'a') → push → ['a'].tags[] → result.tags already []; push 'b' → ['a','b'].{ user: { name: 'Ada' }, tags: ['a','b'] }.Object.fromEntries — silently drops all-but-last for duplicate names; multi-selects and checkbox groups lose data. Merge instead.user[name] as a flat key — you must parse the brackets; otherwise the literal string 'user[name]' becomes the key..value — iterating form.elements yourself means re-implementing successful-control rules (checked, disabled, selected). FormData does it correctly; start there.in vs truthiness for promotion — check key in cur (not cur[key]) so a legitimate first value of ''/0 still promotes correctly on a repeat.items[0][name] / items[][name] (grouping sibling brackets into one element) needs index tracking parseName doesn't do here."5"→5, "true"→true, dates, etc.objectToFormData) flattens a nested object back into bracket names for fetch uploads.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.