DEV Community

Cover image for How to easily convert HTML Form to JSON
Jordan Finneran
Jordan Finneran

Posted on • Updated on • Originally published at jordanfinners.dev

How to easily convert HTML Form to JSON

Contents

  1. Intro
  2. Form Data
  3. Gotcha's
  4. Examples
  5. Summary

Intro

I have a love, but sometimes hate, relationship with HTML Forms. HTML Forms are absolutely brilliant for out of the box validation, accessibility and usability. But they can be a pain to style!
Nevertheless, HTML Forms give you massive amounts of functionality with zero dependencies.

If you are anything like me, you prefer your API to be JSON based. Making it easier and more consistent to deal with requests and responses on the backend.
You could add a middleware to your API endpoint that's going to handle your Form UI but why make that endpoint different from the rest?

What if you could send your Form data off in JSON format and handle it like all your other endpoints. Now you can!

TLDR; Skip to the examples

Form Data

Introducing FormData, this is a really nice Web API for manipulating data in HTML Forms.

This allows us to easily convert a HTML Form to JSON using the following.

  1. Grab the form element from the DOM.
const formElement = document.querySelector('form')
Enter fullscreen mode Exit fullscreen mode
  1. Pass the form to the following function
/**
 * Creates a json object including fields in the form
 *
 * @param {HTMLElement} form The form element to convert
 * @return {Object} The form data
 */
const getFormJSON = (form) => {
  const data = new FormData(form);
  return Array.from(data.keys()).reduce((result, key) => {
    result[key] = data.get(key);
    return result;
  }, {});
};
Enter fullscreen mode Exit fullscreen mode
  1. Action the JSON Object result, however you want to handle the data from the form! Send it off with Fetch for example.

Gotcha's

As with everything these are a few gotchas to look out for!

Checkbox's

If ticked your checkbox's will appear in the result as 'on'. Probably not the boolean you want.
For example, if you've got this input in your form, which the user has checked.

<input name="isOpen" type="checkbox" checked />
Enter fullscreen mode Exit fullscreen mode

It would produce, using the function above:

{
  "isOpen": "on"
}
Enter fullscreen mode Exit fullscreen mode

You will probably want to check for the property and if it equals 'on' and convert it to a boolean.

File Uploads

This one really caught me out, if you've got a file input which accepts multiple files, like so:

<input name="uploads" type="file" multiple />
Enter fullscreen mode Exit fullscreen mode

If one file is uploaded you will get a File object.
But if you have multiple files uploaded, you will actually get a list of them.

Fortunately, there is a really simple fix for this to consistently provide you with a list of files.
Grab the files from the result and process them like so:

[files].flat().filter((file) => !!file.name)
Enter fullscreen mode Exit fullscreen mode

This will consistently give you a list of files, handling if only a single file is uploaded, no file is uploaded or multiples are uploaded.

This also means you can do more client side checks on file sizes and limits for example as well.

Inputs with the same name

Let's say you've got a list of possible tags and a user can tick the ones that apply, perhaps which programming languagues they know, like so:

<input name="tags" type="checkbox" value="javascript" />
<input name="tags" type="checkbox" value="python" />
Enter fullscreen mode Exit fullscreen mode

With the current solution, you would only get the last selected checkbox as the keys would be overridden in the reduce. However there is a simple fix for this as well.

We check to see if the key (the name attribute on the input) already exists in the result, if it does then use a getAll method which will get a list of results.

/**
 * Creates a json object including fields in the form
 *
 * @param {HTMLElement} form The form element to convert
 * @return {Object} The form data
 */
const getFormJSON = (form) => {
  const data = new FormData(form);
  return Array.from(data.keys()).reduce((result, key) => {
    if (result[key]) {
      result[key] = data.getAll(key)
      return result
    }
    result[key] = data.get(key);
    return result;
  }, {});
};
Enter fullscreen mode Exit fullscreen mode

Similarly, to the file upload before, you'll want to handle only one being ticked, no being ticked or multiple, with something like this.

[result.tags || []].flat();
Enter fullscreen mode Exit fullscreen mode

Examples

Shut up and show me the code.

  1. Interactive
  2. Simple
  3. Full
  4. Bonus: Example Test


Simple Example

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
</head>

<body>
  <form name="forms" id="forms">
    <label>Whats your username?
      <input name="username" type="text" />
    </label>
    <label>How many years have you been a developer?
      <input name="age" type="number" />
    </label>
    <button type="submit">Submit</button>
  </form>

  <script>
    // get the form element from dom
    const formElement = document.querySelector('form#forms')

    // convert the form to JSON
    const getFormJSON = (form) => {
      const data = new FormData(form);
      return Array.from(data.keys()).reduce((result, key) => {
        result[key] = data.get(key);
        return result;
      }, {});
    };

    // handle the form submission event, prevent default form behaviour, check validity, convert form to JSON
    const handler = (event) => {
      event.preventDefault();
      const valid = formElement.reportValidity();
      if (valid) {
        const result = getFormJSON(formElement);
        console.log(result)
      }
    }

    formElement.addEventListener("submit", handler)
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode

Full Example

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
</head>

<body>
  <form name="forms" id="forms">
    <label>Whats your username?
      <input name="username" type="text" />
    </label>
    <label>How many years have you been a developer?
      <input name="age" type="number" />
    </label>

    <label>Upload images
      <input name="images" type="file" accept="image/png, image/jpeg" multiple />
    </label>

    <label>Do you know javascript?
      <input name="languages" type="checkbox" value="javascript" />
    </label>
    <label>Do you know python?
      <input name="languages" type="checkbox" value="python" />
    </label>

    <label>Enjoyed this blog?
      <input name="isHappyReader" type="checkbox" />
    </label>

    <button type="submit">Submit</button>
  </form>

  <script>
    // get the form element from dom
    const formElement = document.querySelector('form#forms')

    // convert the form to JSON
    const getFormJSON = (form) => {
      const data = new FormData(form);
      return Array.from(data.keys()).reduce((result, key) => {
        if (result[key]) {
          result[key] = data.getAll(key)
          return result
        }
        result[key] = data.get(key);
        return result;
      }, {});
    };

    // handle the form submission event, prevent default form behaviour, check validity, convert form to JSON
    const handler = (event) => {
      event.preventDefault();
      const valid = formElement.reportValidity();
      if (valid) {
        const result = getFormJSON(formElement);
        // handle one, multiple or no files uploaded
        const images = [result.images].flat().filter((file) => !!file.name)
        // handle one, multiple or no languages selected
        const languages = [result.languages || []].flat();
        // convert the checkbox to a boolean
        const isHappyReader = !!(result.isHappyReader && result.isHappyReader === 'on')

        // use spread function, but override the keys we've made changes to
        const output = {
          ...result,
          images,
          languages,
          isHappyReader
        }
        console.log(output)
      }
    }

    formElement.addEventListener("submit", handler)
  </script>
</body>
Enter fullscreen mode Exit fullscreen mode

Bonus: Example Test

it('should return a JSON representation of a form', () => {
  const form = document.createElement('form');

  const input = document.createElement('input');
  input.name = 'test';
  input.value = 'value';
  form.appendChild(input);

  const number = document.createElement('input');
  number.type = 'number';
  number.name = 'int';
  number.value = '10';
  form.appendChild(number);

  const result = getFormJSON(form);
  expect(result).to.deep.equal({
    test: 'value',
    int: '10',
  });
});
Enter fullscreen mode Exit fullscreen mode

Summary

In summary, you can use what browsers give you to get all the great benefits of HTML Forms, and then convert it to JSON so it's easier to work with the data! I hope this has been useful.

Happy Form building!

Would you be interested in a series of what you can achieve with HTML and Web APIs?
What are you favourite web tips and tricks?

Top comments (13)

Collapse
 
ndrean profile image
NDREAN

you can also try Object.fromEntries([...formData])

Collapse
 
austingil profile image
Austin Gil

In fact, you don't event need the [...]. You can just do Object.fromEntries(formData) :)

Collapse
 
jordanfinners profile image
Jordan Finneran

Oh, nice that's even simpler! If you don't have the 'inputs with same name' gotcha.

Thread Thread
 
austingil profile image
Austin Gil

Right. Do you have examples of inputs with the same name? I can only think of radio inputs, but in that case, it will only take the one that was selected. You probably know some other use-cases though.

Thread Thread
 
jordanfinners profile image
Jordan Finneran

It probably not that common but if you want a list of things a user could check.

For example, if there was a list of languages as checkbox inputs, they could check each one they knew and the output of the Full Example would give you an Array of the languages the user knows.
If you open the Interactive Example in JSFiddle you can see the console output for this :)

Thread Thread
 
ndrean profile image
NDREAN

I have a question; does this make sense if you use name="lang[js]" and name="lang[py]" ?

Thread Thread
 
jordanfinners profile image
Jordan Finneran

You could also do this. It depends what processing you want to do afterwards. :)
I've included both in here jsfiddle.net/8fL3vra1/
Where you can run it, what you get is:

{
  lang[js]: "on",
  lang[py]: "on",
  languages: ["javascript", "python"]
}
Enter fullscreen mode Exit fullscreen mode

My personal preference is to get the list of items but it's completely up to you how you want to achieve it :D

Thread Thread
 
austingil profile image
Austin Gil

Oh yeah, that would make sense. I usually give each checkbox its own name, and if they are related, I put them inside a <fieldset>. I'll need to remember this if I'm working with objects and want a list.

Usually I just wrap the data in a URLSearchParams and send it to the backend like that. URLSearchParams will handle the list if there are multiple entries with the same name.

document.querySelector('form').addEventListener('submit', (e) => {
  const formData = new FormData(e.target)
  const params = new URLSearchParams(formData)

  fetch(`https://example.com?${params.toString()}`)

  fetch(`https://example.com`, {
    method: 'POST',
    body: params
  })

  e.preventDefault()
})
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
ndrean profile image
NDREAN • Edited

If you POST, I believe you can do directly body: new FormData(e.target)
if you GET and adjoin a query string, you may indeed need URLSearchParams

Thread Thread
 
austingil profile image
Austin Gil

Ah yes. This is true. My only issue with this is using FormData in the request body changes the request headers from the default application/x-www-form-urlencoded to multipart/form-data. This has caused me issues before, so I try to avoid it.

Collapse
 
antonio_pangall profile image
Antonio Pangallo

Hi Jordan, nice post. If you use Reactjs you may be also interested on iusehooks.github.io/usetheform/ . It handles forms state in react and much more.

Collapse
 
jordanfinners profile image
Jordan Finneran • Edited

Thank you for the comment. :)
Is this a library you've written?

Collapse
 
antonio_pangall profile image
Antonio Pangallo

Hi, yes I did write it :)