When you call an API from the browser, the body format is part of the contract. FormData vs JSON is one of the first choices that confuses people building forms and SPAs: JSON for REST APIs, FormData for uploads and HTML-shaped posts, and classic form encoding when there is no JavaScript at all. This guide is a practical reference for when to use each, how Content-Type works, and how to debug mismatches.
What is the difference between FormData and JSON?
| JSON | FormData / form encoding | |
|---|---|---|
| Wire format | Text (JSON.stringify) |
Multipart parts or urlencoded key/value pairs |
| Typical header | Content-Type: application/json |
Browser sets multipart/form-data; boundary=... for FormData, or application/x-www-form-urlencoded for classic forms |
| Nested data | Objects and arrays naturally | Flat fields (nesting needs naming tricks) |
| Files | Poor fit (base64 or separate API) | First-class with File / Blob |
HTML <form> default |
No | Yes |
JSON is a data interchange format. FormData is a browser API that builds a multipart/form-data body from fields and files. Related but separate: a plain HTML form without JS often sends application/x-www-form-urlencoded instead of multipart.
When to use JSON for API requests
Use JSON when the server expects a structured document:
- Documented REST or RPC APIs with JSON request bodies
- Nested objects, arrays, booleans, numbers, and
null - SPA or mobile clients talking to your backend with shared types
- No binary file in the same request (or files go to a dedicated upload API)
const payload = {
name: 'Alex',
email: 'alex@example.com',
tags: ['urgent', 'sales'],
meta: { source: 'web' },
};
await fetch('https://api.example.com/leads', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(payload),
});
Do not use JSON as the wire format when you only have a progressive HTML form with no JS, or when the endpoint only parses multipart/urlencoded bodies.
When to use FormData for API requests
Use FormData when you need form-shaped or file-shaped traffic:
- File or image uploads with extra text fields in one request
- Building the same payload a real
<form>would send - Endpoints that document
multipart/form-dataor “form fields” - Collecting values from an existing form via
new FormData(formElement)
const form = document.querySelector('#contact');
const data = new FormData(form);
// data.append('name', 'Alex');
// data.append('file', fileInput.files[0]);
await fetch('https://api.example.com/upload', {
method: 'POST',
body: data,
// Do not set Content-Type manually. The browser adds the multipart boundary.
});
Do not use FormData when the API only accepts JSON schemas, or when you need deep nested structures without painful field names.
When to use a plain HTML form (no FormData API)
If the page can work without JavaScript, prefer a normal form POST. The browser encodes fields for you:
<form action="https://api.example.com/submit" method="POST">
<input type="text" name="name" required />
<input type="email" name="email" required />
<textarea name="message" required></textarea>
<button type="submit">Submit</button>
</form>
- Default
enctypeisapplication/x-www-form-urlencoded. - Use
enctype="multipart/form-data"when the form includes<input type="file">.
This is still “form encoding,” not JSON. Many contact and marketing endpoints are built for this shape.
FormData vs URLSearchParams vs JSON (quick map)
| Tool | Typical use |
|---|---|
JSON.stringify + application/json |
App APIs, nested data |
FormData |
Multipart, files + fields |
URLSearchParams |
Urlencoded bodies or query strings without files |
Native <form method="POST"> |
No-JS submit, progressive enhancement |
Example urlencoded body with URLSearchParams (no files):
const body = new URLSearchParams({
name: 'Alex',
email: 'alex@example.com',
});
await fetch('https://api.example.com/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
});
Common Content-Type mistakes
-
Setting
Content-Type: multipart/form-datayourself
You omit the boundary. Let the browser set the header whenbodyisFormData. -
Passing a plain object as
fetchbody for JSON
UseJSON.stringify. Do not assume every runtime serializes objects for you. -
JSON.stringifyon aFormDatainstance
Produces junk text, not multipart parts. -
JSON client against a form-only server (or the reverse)
Empty body on the server is usually an encoding mismatch, not “wrong field names only.” -
CORS surprises after switching to JSON
application/jsonoften triggers a preflight. Simple form posts may not. Check preflight responses when debugging.
Example: read a form with FormData, send JSON to your API
In SPAs you often collect inputs from a form, then talk to a JSON API:
async function onSubmit(event) {
event.preventDefault();
const form = event.currentTarget;
const payload = Object.fromEntries(new FormData(form).entries());
await fetch('/api/leads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
}
Here FormData only reads fields. The network body is still JSON because that is what /api/leads expects.
Example: keep form encoding end to end
When the receiver is a form endpoint, keep form encoding on the wire. Build FormData (or use a native form POST) and do not force application/json.
async function onSubmit(event) {
event.preventDefault();
const data = new FormData(event.currentTarget);
await fetch('https://api.example.com/form-endpoint', {
method: 'POST',
body: data,
});
}
For contact forms on static sites and marketing pages, the no-JS HTML action + method="POST" pattern is often enough. See contact form on a static website without a backend if you want that path without standing up your own API.
Decision checklist: FormData or JSON?
- API docs say JSON? →
JSON.stringify+application/json. - Uploading files in this request? →
FormData(multipart), unless there is a separate upload protocol. - Must work with no JavaScript? → Native HTML form POST, not a JSON-only client.
- Endpoint is a form backend? → Form fields (urlencoded or multipart), not a JSON blob.
- Need nested objects and arrays? → JSON.
FAQ
Is FormData the same as multipart/form-data?
Almost always in browsers: new FormData() produces a multipart body and the correct boundary. Classic forms without files often use urlencoded instead.
Can I send JSON from an HTML form without JavaScript?
Not as a real application/json body. Without JS, the browser sends form encoding. To send JSON you need script (or a server that accepts form fields).
Should I always prefer JSON in modern apps?
No. Prefer whatever the endpoint documents. Modern apps still use multipart for uploads and form posts for progressive HTML.
Why is my server body empty?
Open DevTools → Network → the request payload. Confirm JSON text vs form fields, then match the server parser (JSON vs multipart vs urlencoded).
Does Object.fromEntries(new FormData(form)) support multiple values for one name?
Object.fromEntries keeps only the last value per key. For multi-selects or repeated names, iterate formData.getAll(name) instead.
Encoding is a contract, not a style choice
FormData vs JSON is about matching the client body to the server parser: nested app APIs lean JSON; files and HTML forms lean FormData or native form encoding. Get the Content-Type and body shape right before you debug business logic.
When the job is a website contact form and you do not want to own a JSON lead API plus mail infrastructure, a form-shaped endpoint is the natural fit: post fields as a form, filter spam on the server, and deliver notifications without a custom backend. FormsReach is built for that path (email and free webhooks on Free with 500 credits/month; WhatsApp via Meta’s Business API on Pro and Agency). For a full form-to-notification walkthrough, see send form submissions to WhatsApp.
