How to Convert HTML Form Field Values to a JSON Object

A simple, dependency-free way to use reduce() to convert all the fields in a form to a JSON object for AJAX (or whatever).

Using AJAX is really common, but it’s still tricky to get the values out of a form without using a library.

And that’s because it seems pretty intimidating to set up all the loops and checks required to deal with parsing a form and all its child elements. You get into heavy discussions of whether you should use for, for...in, for...of, or forEach, and after trying to keep up with the various performance, semantic, and stylistic reasons for making those choices, your brain starts to liquefy and drip out your ears — at which point it’s easy to just say, “Fuck it; let’s just use jQuery.”

But for simple sites that don’t need much beyond grabbing form data as an object to use with JSON, jQuery (or any big library or framework) includes a lot of overhead for only one or two functions that you’ll be using.1

So in this walkthrough, we’ll be writing our own script — in plain JavaScript — to pull the values of a form’s fields into an object, which we could then use for AJAX, updating information on other parts of the page, and anything else you might want to do with a form’s data.

What We’ll Be Building

At the end of this walkthrough, we’ll have built the form shown in this pen:

If you fill the form and hit the “Send It!” button, the form data will be output as JSON in the “Form Data” section below.

Before We Get Started: Goals and Plans

To save ourselves a lot of headache and heartache, we’re going to start our project with an clear plan. This’ll keep our goals clear, and helps define the structure and purpose of the code before we ever write a line.

Start with a goal: what should we end up with?

Before we write any JavaScript, let’s start by deciding how we want the output to look.

If I’ve filled out the form above completely, we’d want the resulting object to look like this:

{
  "salutation": "Mr.",
  "name": "Jason Lengstorf",
  "email": "jason@lengstorf.com",
  "subject": "I have a general question.",
  "message": "Is this thing on?",
  "snacks": [
    "pizza"
  ],
  "secret": "1b3a9374-1a8e-434e-90ab-21aa7b9b80e7"
}

Each field’s name attribute is used as the object’s key, and the field’s value is set as the object’s value.

This is ideal, because it means that we can do something like this:

// Find our form in the DOM using its class name.
const form = document.getElementByClassName('.contact-form')[0];

// Get the form data with our (yet to be defined) function.
const data = getFormDataAsJSON(form);

// Do something with the email address.
doSomething(data.email);

This is straightforward, easy to read as a human, and also easy to send to APIs that accept application/json data in requests (which is most of them these days).

So let’s shoot for that.

Make a plan: how can we convert form fields to JSON?

When we’re finished, our JavaScript should accomplish the following goals:

  1. Capture the form’s submit event and prevent the default submission.
  2. Convert the form’s child elements to JSON.
  3. Check to make sure only form field elements are added to the object.
  4. Add a safeguard to only store checkable fields if the checked attribute is set.
  5. Handle inputs that allow multiple values, like checkboxes.

Ready to flex that sexy-ass brain? Create a fork of the markup-and-styles-only pen, and let’s jump in and start writing some JavaScript.

Getting Started: Create a Form for Testing

To avoid the hassle of setting up front-end tooling (we’re using Babel to transpile the newer features of JavaScript, such as fat-arrow functions), we’re going to work through this project on Codepen.

To start, create a fork of this pen, which contains form markup with common inputs, and some styles to make it display nicely.

Step 1: Add a Listener to the submit Event for the Form

Before we do anything else, we need to listen for the submit event on our form, and prevent it from doing its usual thing.

To do this, let’s create a function called handleSubmit(), then use getElementsByClassName() to find our form, and attach the function to the form’s submit event.

Create a handleSubmit() function.

At the moment, this function isn’t going to do much. To start, we’ll prevent the default submit action, create a variable called data to store the output (which we’ll be building in a moment), then find our output container and print out the data variable as JSON.

In order to prevent the default action, this function needs to accept one argument: the event that’s created when the user clicks the submit button on the form. We can stop the form from submitting the usual way (which triggers the browser to go somewhere else) using event.preventDefault().

/**
 * A handler function to prevent default submission and run our custom script.
 * @param  {Event} event  the submit event triggered by the user
 * @return {void}
 */
const handleFormSubmit = event => {
  
  // Stop the form from submitting since we’re handling that with AJAX.
  event.preventDefault();
  
  // TODO: Call our function to get the form data.
  const data = {};
  
  // Demo only: print the form data onscreen as a formatted JSON object.
  const dataContainer = document.getElementsByClassName('results__display')[0];
  
  // Use `JSON.stringify()` to make the output valid, human-readable JSON.
  dataContainer.textContent = JSON.stringify(data, null, "  ");
  
  // ...this is where we’d actually do something with the form data...
};

Attach an event listener to the form.

With the event handler created, we need to add a listener to the form so we can actually handle the event.

To do this, we use getElementsByClassName() to target the form, then store the first item in the resulting collection as form.

Next, using addEventListener(), we hook handleSubmit() to the submit event, which will allow it to run whenever the user clicks to submit the form.

const handleFormSubmit = event => {/* omitted for brevity */};

/*
 * This is where things actually get started. We find the form element using
 * its class name, then attach the `handleFormSubmit()` function to the 
 * `submit` event.
 */
const form = document.getElementsByClassName('contact-form')[0];
form.addEventListener('submit', handleFormSubmit);

At this point we can test that things are working properly by clicking the “Send It!” button on the form. We should see {} in the “Form Data” output box.

Step 2: Extract the Values of Form Fields As JSON

Next up, we need to actually grab values from the form fields.

To do this, we’ll use something that — at first — might look scary as shit: reduce() combined with call().

We’ll dive into the dirty details of what reduce() is actually doing in the next section, but for now let’s focus on how we’re actually using it.

/**
 * Retrieves input data from a form and returns it as a JSON object.
 * @param  {HTMLFormControlsCollection} elements  the form elements
 * @return {Object}                               form data as an object literal
 */
const formToJSON = elements => [].reduce.call(elements, (data, element) => {
  
  data[element.name] = element.value;
  return data;

}, {});

const handleFormSubmit = event => {
  
  // Stop the form from submitting since we’re handling that with AJAX.
  event.preventDefault();
  
  // Call our function to get the form data.
  const data = formToJSON(form.elements);

  // Demo only: print the form data onscreen as a formatted JSON object.
  const dataContainer = document.getElementsByClassName('results__display')[0];
  
  // Use `JSON.stringify()` to make the output valid, human-readable JSON.
  dataContainer.textContent = JSON.stringify(data, null, "  ");
  
  // ...this is where we’d actually do something with the form data...
};

const form = document.getElementsByClassName('contact-form')[0];
form.addEventListener('submit', handleFormSubmit);

I know. I know. It looks hairy. But let’s dig in and see what this is doing.

First, let’s break this into its component parts:

  1. We have a function called formToJSON(), which accepts one argument: form
  2. Inside that function, we return the value of [].reduce.call(), which accepts three arguments: a form, a function, and an empty object literal ({})
  3. The function argument accepts the arguments data and child, and adds a new property with the key of child.name and the value child.value, finally returning the data object

After we’ve added that code to our pen, we need to call the function from handleSubmit(). Find const data = {}; inside the function and replace it with const data = formToJSON(form.elements);.

Now we can run it by clicking the “Send It!” button will now output this:

{
  "salutation": "Ms.",
  "name": "",
  "email": "",
  "subject": "I have a problem.",
  "message": "",
  "snacks": "cake",
  "secret": "1b3a9374-1a8e-434e-90ab-21aa7b9b80e7",
  "": ""
}

There are some issues here — for example, neither “Ms.” nor “Cake” was actually selected on the form, and there’s an empty entry at the bottom (which is our button) — but this isn’t too bad for a first step.

So how did that just happen? Let’s go step by step to figure it out.

Step 2.1 — Understand how reduce() works.

The simplest explanation for reduce() is this:

The reduce() method uses a function to convert an array into a single value.

This method is part of the Array prototype, so it can be applied to any array value.

It takes two arguments:

  1. A reducer function, which is required.
  2. An initial value, which is optional (defaults to 0).

The reducer function is applied to each element of the array. This function accepts four arguments:

  1. The value returned by the reducer function when it ran on the previous element (or the initial value, if this is the first element).
  2. The current array element.
  3. The current array index.
  4. The whole array, in case the reducer needs a reference to it.

For our reducer, we only need the first two arguments.

A really simple example of reducing an array.

Let’s say we have an array of numbers, which represent sales for the day:

const sales = [100.12, 19.49, 10, 42.18, 99.62];

We need to determine total sales for the day, so we set up this simple function to add up sales:

function getTotalSales(previousTotal, currentSaleAmount) {
    return previousTotal + currentSaleAmount;
}

Then we use reduce() to apply the function to the array of sales:

const sales = [100.12, 19.49, 10, 42.18, 99.62];

function getTotalSales(previousTotal, currentSaleAmount) {
    return previousTotal + currentSaleAmount;
}

sales.reduce(getTotalSales);
// result: 271.41

Now, if we want to condense this code a little, we can actually write the whole thing like this:

const sales = [100.12, 19.49, 10, 42.18, 99.62];
sales.reduce((prev, curr) => prev + curr);
// result: 271.41

When this is called, reduce() starts with 0 as the value of prev, and takes the first element of the array, 100.12, as the value of curr. It adds those together and returns them.

Now reduce() moves to the second element in the array, 19.49, and this time the value of prev is the value returned last time: 100.12.

This process is repeated until all of the elements have been added together, and we end up with our total sales for the day: 271.41.

Step 2.2 — Deconstruct the function.

As it stands, formToJSON() is actually made of three parts:

  1. A reducer function to combine our form elements into a single object.
  2. An initial value of {} to hold our form data.
  3. A call to reduce() using call(), which allows us to force reduce() to work with elements, even though it’s technically not an array.

Step 2.3 — Write the reducer function.

First up, we need to have our reducer function. In the simple example of reducing an array, we used single values, which won’t work in this case. Instead, we want to add each field to an object with a format like this:

{
    field_name: "field_value",
}

So our reducer function works like this:

// This is the function that is called on each element of the array.
const reducerFunction = (data, element) => {

  // Add the current field to the object.
  data[element.name] = element.value;

  // For the demo only: show each step in the reducer’s progress.
  console.log(JSON.stringify(data));

  return data;
};

The data object is the previous value of the reducer, and element is the current form element in the array. We then add a new property to the object using the element’s name property — this is the input’s name attribute in the HTML — and store its value there.

When we return data, we make the updated object available to the next call of the funciton, which allows us to add each field to the object, one by one.

Step 2.4 — Call the reducer.

To make it a little more obvious what’s happening in the formToJSON() function, here’s what it looks like if we break it up into more verbose code:

const formToJSON_deconstructed = elements => {
  
  // This is the function that is called on each element of the array.
  const reducerFunction = (data, element) => {
    
    // Add the current field to the object.
    data[element.name] = element.value;
    
    // For the demo only: show each step in the reducer’s progress.
    console.log(JSON.stringify(data));

    return data;
  };
  
  // This is used as the initial value of `data` in `reducerFunction()`.
  const reducerInitialValue = {};
  
  // To help visualize what happens, log the inital value.
  console.log('Initial `data` value:', JSON.stringify(reducerInitialValue));
  
  // Now we reduce by `call`-ing `Array.prototype.reduce()` on `elements`.
  const formData = [].reduce.call(elements, reducerFunction, reducerInitialValue);
  
  // The result is then returned for use elsewhere.
  return formData;
};

In the above example, we do exactly the same thing as in formToJSON(), but we’ve broken it down into its component parts.

We can see the output if we update handleSubmit() and change the call to formToJSON(form.elements) to formToJSON_deconstructed(form.elements). Check the console to see this output:

Initial `data` value: {}
{"salutation":"Mr."}
{"salutation":"Mrs."}
{"salutation":"Ms."}
{"salutation":"Ms.","name":""}
{"salutation":"Ms.","name":"","email":""}
{"salutation":"Ms.","name":"","email":"","subject":"I have a problem."}
{"salutation":"Ms.","name":"","email":"","subject":"I have a problem.","message":""}
{"salutation":"Ms.","name":"","email":"","subject":"I have a problem.","message":"","snacks":"pizza"}
{"salutation":"Ms.","name":"","email":"","subject":"I have a problem.","message":"","snacks":"cake"}
{"salutation":"Ms.","name":"","email":"","subject":"I have a problem.","message":"","snacks":"cake","secret":"1b3a9374-1a8e-434e-90ab-21aa7b9b80e7"}
{"salutation":"Ms.","name":"","email":"","subject":"I have a problem.","message":"","snacks":"cake","secret":"1b3a9374-1a8e-434e-90ab-21aa7b9b80e7","":""}

We can see here that the reducer is called for every form element, and the object grows with each subsequent call until we’ve got an entry for every name value in the form.

Change handleSubmit() back to using formToJSON(form.elements), and let’s move on to cleaning up this output to only include fields it should include.

Step 3: Add a Check to Make Sure Only the Fields We Want Are Collected

The first problem we can see in the output is that fields with both empty name and empty value attributes have been added to the array. This isn’t what we want in this case, so we need to add a quick check to verify that fields have both a name and a value before we add them.

Step 3.1 — Create a function to check for valid elements.

First, let’s add a new function to our pen called isValidElement(). This function will accept one argument — the element — and return either true or false.

To return true, the element must have:

  1. A non-empty name property.
  2. A non-empty value property.

Implement this check like so:

/**
 * Checks that an element has a non-empty `name` and `value` property.
 * @param  {Element} element  the element to check
 * @return {Bool}             true if the element is an input, false if not
 */
const isValidElement = element => {
  return element.name && element.value;
};

Pretty simple, right?

This gives us a flag that lets us avoid unused elements (like the button) and unfilled fields (such as an empty Email field) from being added to the form data object.

Step 3.2 — Add the check to formToJSON().

Next, we need to add an if check for whether or not our element is valid in formToJSON(). Since we don’t want to add anything if the element is not valid, we can simply do the following:

const formToJSON = elements => [].reduce.call(elements, (data, element) => {
  
  // Make sure the element has the required properties.
  if (isValidElement(element)) {
    data[element.name] = element.value;
  }

  return data;
}, {});

Now when we submit our form, the output is much cleaner:

{
  "salutation": "Ms.",
  "subject": "I have a problem.",
  "snacks": "cake",
  "secret": "1b3a9374-1a8e-434e-90ab-21aa7b9b80e7"
}

However, we’re still not there yet. In the next step, we’ll deal with checkable elements like radio inputs and checkboxes.

Step 4: Only Store Checkable Fields If a Field Is In checked State

Now we need another check to identify whether or not an element should be added to the array. For instance, right now the salutation field is being stored with the value Ms., even though that value is not selected in the form.

Obviously, this is bad news. So let’s fix it.

Step 4.1 — Create a function to check for checkable elements.

First, let’s add a new function to check whether or not an element’s value should be considered valid for inclusion in the object.

Our criteria for determining a “valid” element are:

  1. The element is not a checkbox or radio input.
  2. If the element is a checkbox or radio input, it has the checked attribute.

Add the following to create this check:

/**
 * Checks if an element’s value can be saved (e.g. not an unselected checkbox).
 * @param  {Element} element  the element to check
 * @return {Boolean}          true if the value should be added, false if not
 */
const isValidValue = element => {
  return (!['checkbox', 'radio'].includes(element.type) || element.checked);
};

Step 4.2 — Add the check to formToJSON().

Now we can add this check to formToJSON(), which is as simple as adding a second condition to our existing if check:

const formToJSON = elements => [].reduce.call(elements, (data, element) => {
  
  // Make sure the element has the required properties and should be added.
  if (isValidElement(element) && isValidValue(element)) {
    data[element.name] = element.value;
  }

  return data;
}, {});

Now we can run our code and see that the output is much cleaner:

{
  "subject": "I have a problem.",
  "secret": "1b3a9374-1a8e-434e-90ab-21aa7b9b80e7"
}

This is much better — now we only get elements that actually have a value set.

Step 5: If a Field Allows Multiple Values, Store Them In an Array

But we’re not quite done yet, because the form is still fucking up the snacks field — which is clearly the most important field.

Try selecting both “Pizza” and “Cake” to see the problem:

{
  "subject": "I have a problem.",
  "snacks": "cake",
  "secret": "1b3a9374-1a8e-434e-90ab-21aa7b9b80e7"
}

Nope. This is a disaster. We need both pizza AND cake. So let’s make sure that can happen.

Step 5.1 — Create checks for elements that accept multiple values.

The check for whether or not multiple values are allowed has two parts, because there are two elements that allow multiple values.

First, we need to add a check for any checkboxes. This is simple enough: we just check if the type is checkbox:

/**
 * Checks if an input is a checkbox, because checkboxes allow multiple values.
 * @param  {Element} element  the element to check
 * @return {Boolean}          true if the element is a checkbox, false if not
 */
const isCheckbox = element => element.type === 'checkbox';

Second, we need to add a check for a select element with the multiple attribute.

This is a bit trickier, but still pretty straightforward. A select has a property called options, so we’ll check for that first. Next, we check for the multiple property. If both exist, our check will return true:

/**
 * Checks if an input is a `select` with the `multiple` attribute.
 * @param  {Element} element  the element to check
 * @return {Boolean}          true if the element is a multiselect, false if not
 */
const isMultiSelect = element => element.options && element.multiple;

Step 5.2 — Handle checkboxes in formToJSON().

Inside formToJSON(), we need to add another if block for our isCheckbox() function.

If the current element is a checkbox, we need to store its value(s) in an array. Let’s take a look at the code first, and then we’ll talk about how it works.

const formToJSON = elements => [].reduce.call(elements, (data, element) => {

  // Make sure the element has the required properties and should be added.
  if (isValidElement(element) && isValidValue(element)) {

    /*
     * Some fields allow for more than one value, so we need to check if this
     * is one of those fields and, if so, store the values as an array.
     */
    if (isCheckbox(element)) {
      
      data[element.name] = (data[element.name] || []).concat(element.value);
    } else {
      data[element.name] = element.value;
    }
  }

  return data;
}, {});

Since we need to get the element’s values into an array, we use a bit of shorthand in (data[element.name] || []), which means, “use the existing array, or a new, empty one”.

Then we use concat() to add the current value to the array.

Now if we check the options for both pizza and cake, we see the following:

{
  "subject": "I have a problem.",
  "snacks": [
    "pizza",
    "cake"
  ],
  "secret": "1b3a9374-1a8e-434e-90ab-21aa7b9b80e7"
}

Much better. Crisis averted, everyone!

Step 5.3 — Write a function to retrieve values from multi-selects.

Our very last step before we can call this sucker done is to add a check for select fields that support multiple selected options. I’m not a big fan of this input type, because I think it’s a confusing input for people to use — one that’s easily replaced with checkboxes — but for the sake of covering bases we’ll plug it in.

The selected options from a multi-select are stored in their own array-like object, called an HTMLOptionsCollection, so we need to run reduce() on this as well.

Let’s keep things clean by moving this out into its own function.

This function will accept each options, check if the selected property is true, then add its value to an array called values, which will ultimately be returned containing the values of all selected options.

Add the following to the pen:

/**
 * Retrieves the selected options from a multi-select as an array.
 * @param  {HTMLOptionsCollection} options  the options for the select
 * @return {Array}                          an array of selected option values
 */
const getSelectValues = options => [].reduce.call(options, (values, option) => {
  return option.selected ? values.concat(option.value) : values;
}, []);

Step 5.4 — Handle multi-select values in formToJSON().

To put a bow on all this, we need to add an else if block in our formToJSON() function.

After the isCheckbox() check, we’ll add a isMultiSelect() check. If that returns true, we’ll add the select’s values to the object as an array using getSelectValues().

Make the following updates to the pen:

const formToJSON = elements => [].reduce.call(elements, (data, element) => {

  // Make sure the element has the required properties and should be added.
  if (isValidElement(element) && isValidValue(element)) {

    /*
     * Some fields allow for more than one value, so we need to check if this
     * is one of those fields and, if so, store the values as an array.
     */
    if (isCheckbox(element)) {
      data[element.name] = (data[element.name] || []).concat(element.value);
    } else if (isMultiSelect(element)) {
      data[element.name] = getSelectValues(element);
    } else {
      data[element.name] = element.value;
    }
  }

  return data;
}, {});

Run a quick test of multi-select values.

Since our current form doesn’t have a select with the multiple attribute, so let’s quickly add that to the subject field in our pen.

Look for the select in the HTML pane and add the multiple attribute like so:

      select#subject.contact-form__input.contact-form__input--select(
        name="subject"
        multiple
      )

Now we can actually test. Click on both options and submit the form. The out put will now be:

{
  "subject": [
    "I have a problem.",
    "I have a general question."
  ],
  "secret": "1b3a9374-1a8e-434e-90ab-21aa7b9b80e7"
}

After we’ve tested, we can remove the multiple attribute from the select input.

The Final Result: Form Field Values Are Collected in an Object for Use as JSON

At this point, we’ve built a small script that will extract the values from a form as an object literal, which can easily be converted to JSON using JSON.stringify().

We can test by filling out our form with dummy data and submitting it.

Use your own fork of the pen, or enter dummy data in the form below:

After submitting, we’ll see the info we entered, and it’ll look something like the following:

{
  "salutation": "Ms.",
  "name": "Pac-Man",
  "email": "mspacman@example.com",
  "subject": "I have a problem.",
  "message": "These ghosts keep chasing me!",
  "snacks": [
    "pizza",
    "cake"
  ],
  "secret": "1b3a9374-1a8e-434e-90ab-21aa7b9b80e7"
}

  1. And even if it’s not something we’d ever use in production, writing our own utility scripts is a fantastic way to increase our understanding of how things work. If we rely too much on a tool’s “magic” to make our apps work, it becomes really hard to debug them when we find a problem that falls outside of the tool’s scope.

Question? Ideas for improvements? Spotted a bug?

The code in this article is available on Codepen. You can add a comment, fork, or favorite this pen. I'd really love to hear your ideas for optimizations, improvements, or improved clarity.