You have several checkboxes that share a name, such as topic, or a select with the multiple attribute. A user ticks two boxes, your script turns the form into JSON, and the server only ever sees one of them. This is the usual culprit, and the fix is a small helper:
function formToObject(form, multiKeys = []) {
const fd = new FormData(form);
const out = {};
for (const key of multiKeys) out[key] = [];
for (const key of new Set(fd.keys())) {
out[key] = multiKeys.includes(key) ? fd.getAll(key) : fd.get(key);
}
return out;
}
// Buggy version for comparison:
// Object.fromEntries(new FormData(form))The cause
The browser does its part. Each checked box, and each selected option in a multi-select, becomes its own entry in the FormData, all under the same name. The loss happens when you flatten that list into a plain object, which can hold one value per key. Object.fromEntries lets a later entry overwrite an earlier one, so with two boxes checked you keep the last. FormData.get() has the opposite quirk: per MDN, it returns only the first value for a key and points you to getAll() for the rest.
Run against a form with two checked topic boxes and a multi-select with two selected options, the buggy version produced {"topic":"webforms","tools":"b"}, while the helper above produced {"topic":["esign","webforms"],"tools":["a","b"]}. That was tested in Node with jsdom.
Caveats
You have to name the repeated fields yourself, because a lone checked box looks identical to a single-value field in a FormData. Listing them in multiKeys also gives you an empty array when nothing is checked. Without that, an unchecked group sends no entry at all and the key is simply missing from your object.
If you submit the form natively instead of building JSON, repeated names reach the server intact. Read them with your framework's accessor for all values of a key, not its single-value one, or you will lose the extras on that side instead.
Back to All Questions