Skip to content

Form data is returned in as strings, not in correct TS types #6517

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
IgorArnaut opened this issue May 18, 2025 · 4 comments
Open

Form data is returned in as strings, not in correct TS types #6517

IgorArnaut opened this issue May 18, 2025 · 4 comments

Comments

@IgorArnaut
Copy link

IgorArnaut commented May 18, 2025

I have this complicated form and I send data from it.

<form action="/oglasi/postavka" method="post">
  <fieldset>
    <legend>Informacije o zgradi</legend>
    <div>
      <label for="address">Adresa:</label>
      <input type="text" name="building[address]" id="address" maxlength="255" required>
    </div>
    <div>
      <label for="constructedIn">Izgrađen:</label>
      <input type="number" name="building[constructedIn]" id="constructedIn" required>
    </div>
    <div>
      <label for="numOfFloors">Spratnost:</label>
      <input type="number" name="building[numOfFloors]" id="numOfFloors" required>
    </div>
    <div>
      <label for="hasParking">Parking:</label>
      <input type="checkbox" name="building[hasParking]" id="hasParking">
    </div>
    <div>
      <label for="hasGarage">Garaža:</label>
      <input type="checkbox" name="building[hasGarage]" id="hasGarage">
    </div>
    <div>
      <label for="hasElevator">Lift:</label>
      <input type="checkbox" name="building[hasElevator]" id="hasElevator">
    </div>
    <div>
      <label for="hasCctv">Video nadzor:</label>
      <input type="checkbox" name="building[hasCctv]" id="hasCctv">
    </div>
    <div>
      <label for="hasIntercom">Interfon:</label>
      <input type="checkbox" name="building[hasIntercom]" id="hasIntercom">
    </div>
  </fieldset>

  <fieldset>
    <legend>Informacije o stanu</legend>
    <div>
      <label for="location">Lokacija:</label>
      <input type="text" name="apartment[location]" id="location" maxlength="255" required>
    </div>
    <div>
      <label for="floor">Sprat:</label>
      <input type="number" name="apartment[floor]" id="floor" required>
    </div>
    <div>
      <label for="area">Površina:</label>
      <input type="number" name="apartment[area]" id="area" required>
    </div>
    <div>
      <label for="price">Cena:</label>
      <input type="number" name="apartment[price]" id="price" required>
    </div>
    <div>
      <label for="numOfRooms">Broj soba:</label>
      <input type="number" name="apartment[numOfRooms]" id="numOfRooms" required>
    </div>
    <div>
      <label for="state">Stanje:</label>
      <select name="apartment[state]" id="state">
        <option value="Izvorno">Izvorno</option>
        <option value="Novo">Novo</option>
        <option value="Renovirano">Renovirano</option>
        <option value="Lux">Lux</option>
      </select>
    </div>
    <div>
      <label for="heating">Grejanje:</label>
      <select name="apartment[heating]" id="heating">
        <option value="Gradsko">Gradsko</option>
        <option value="Etažno">Etažno</option>
        <option value="Podno">Podno</option>
        <option value="Struja">Struja</option>
        <option value="Gas">Gas</option>
        <option value="TA">TA</option>
      </select>
    </div>
    <div>
      <label for="equipment">Opremljenost:</label>
      <select name="apartment[equipment]" id="equipment">
        <option value="Namešten">Namešten</option>
        <option value="Polunamešten">Polunamešten</option>
        <option value="Prazan">Prazan</option>
      </select>
    </div>
    <div>
      <label for="items">Stvari:</label>
      <select name="apartment[items]" id="items" multiple>
        <% items.forEach(item => { %>
        <option value="<%- item.id %>"><%- item.name %></option>
        <% }) %>
      </select>
    </div>
  </fieldset>
  <fieldset>
    <legend>Uslovi zakupa</legend>
    <div>
      <label for="availableOn">Useljiv:</label>
      <input type="date" name="terms[availableOn]" id="availableOn" required>
    </div>
    <div>
      <label for="hasDeposit">Depozit:</label>
      <input type="checkbox" name="terms[hasDeposit]" id="hasDeposit">
    </div>
    <div>
      <label for="isForStudents">Za studente:</label>
      <input type="checkbox" name="terms[isForStudents]" id="isForStudents">
    </div>
    <div>
      <label for="isForWorkers">Za radnike:</label>
      <input type="checkbox" name="terms[isForWorkers]" id="isForWorkers">
    </div>
    <div>
      <label for="isSmokingAllowed">Dozvoljeno pušenje:</label>
      <input type="checkbox" name="terms[isSmokingAllowed]" id="isSmokingAllowed">
    </div>
    <div>
      <label for="arePetsAllowed">Dozvoljeni ljubimci:</label>
      <input type="checkbox" name="terms[arePetsAllowed]" id="arePetsAllowed">
    </div>
  </fieldset>
  <fieldset>
    <legend>Oglas</legend>
    <div>
      <label for="title">Naslov:</label>
      <input type="text" name="listing[title]" maxlength="255" required id="title">
    </div>
    <div>
      <label for="description">Opis:</label>
      <textarea name="listing[description]" cols="40" rows="10" required id="description"></textarea>
    </div>
  </fieldset>
  <input type="submit" value="<%= value %>">
</form>

The route that handles the POST of this form looks like this:

router.post("/postavka", async (req, res, next) => {
  const reqBody = req.body;
  const { building, apartment, terms, listings } = reqBody;

  for (let field in building) {
    console.log(field);
  }
  console.log(reqBody);
  res.status(200).json(reqBody);
});

The resulting data looks like this:

{
  "building": {
    "address": "Alekse Santica 4, Novi Sad",
    "constructedIn": "2009",
    "numOfFloors": "1",
    "hasParking": "on",
    "hasCctv": "on",
    "hasIntercom": "on"
  },
  "apartment": {
    "location": "Grbavica",
    "floor": "0",
    "area": "30",
    "price": "30",
    "numOfRooms": "0",
    "state": "Izvorno",
    "heating": "Gradsko",
    "equipment": "Namešten",
    "items": [
      "1",
      "2",
      "16"
    ]
  },
  "terms": {
    "availableOn": "2025-01-01",
    "hasDeposit": "on",
    "isForStudents": "on"
  },
  "listing": {
    "title": "A",
    "description": "A."
  }
}

Everything is returned as string. Checkbox values should be boolean, even false if they are unchecked. Number input values should be number. Date input values should be Date. I need to pass this data into Prisma ORM functions.

I know that it's a default JS thing and that it should be manually handled, but it would be a waste of time to convert all of the values, especially when you have a complex form data such as this. There are libraries busboy, multiparty, formidable, multer, but from what I've seen, those packages are primarily used for FILE uploads. I have not seen examples of those packages being used for such forms. I've also seen examples that used only text inputs. I've tried formidable, and even this package returned form data in wrong types.

I made a form parser that converts data into correct types, but it doesn't show unchecked checkbox values as false:

export default (obj) => {
  for (let field in obj) {
    if (!isNaN(obj[field])) obj[field] = Number(obj[field]);
    if (obj[field] == "on") obj[field] = true;
    const regexp = RegExp("^d{4}-d{2}-d{2}$");
    if (regexp.test(obj[field])) obj[field] = new Date(obj[field]);
  }

  console.log(obj);
  return obj;
};

Environment information

Version:

"dependencies": {
    "@prisma/client": "^6.7.0",
    "body-parser": "^2.2.0",
    "debug": "~2.6.9",
    "ejs": "^3.1.10",
    "express": "^4.21.2",
    "http-errors": "~1.6.3",
    "morgan": "~1.9.1",
    "pug": "^3.0.3"
},
"devDependencies": {
    "@types/body-parser": "^1.19.5",
    "@types/debug": "^4.1.12",
    "@types/ejs": "^3.1.5",
    "@types/express": "^5.0.1",
    "@types/http-errors": "^2.0.4",
    "@types/morgan": "^1.9.9",
    "@types/node": "^22.15.3",
    "@types/pug": "^2.0.10",
    "prisma": "^6.7.0",
    "tsx": "^4.19.4",
    "typescript": "^5.8.3"
},

Platform:
Windows: Microsoft Windows NT 10.0.26100.0 x64

Node.js version:
v22.15.0

What steps will reproduce the bug?

  1. Create an Express project
  2. Create a page with a form
  3. Create get (to insert existing values into form, e.g. select input) and post routes for form
  4. Send form data to Express with POST
  5. Display form data as JSON
  6. It's all strings.
@IgorArnaut IgorArnaut added the bug label May 18, 2025
@yusufansari563
Copy link

yusufansari563 commented May 18, 2025

Can you please check if you are adding express json middleware or not?
i think you are missing this

app.use(express.json());

Complete structure below

import express from 'express';

const app = express();

app.use(express.json());

app.post("/postavka", async (req, res, next) => {
    console.log(req.body);
})

@IgorArnaut
Copy link
Author

IgorArnaut commented May 18, 2025

Can you please check if you are adding express json middleware or not? i think you are missing this

app.use(express.json());

Complete structure below

import express from 'express';

const app = express();

app.use(express.json());

app.post("/postavka", async (req, res, next) => {
    console.log(req.body);
})

I use body-parser in the middleware.

const app = express();

const __dirname = import.meta.dirname;

// view engine setup
app.set("views", join(__dirname, "views"));
app.set("view engine", "ejs");

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(logger("dev"));
app.use(express.static(join(__dirname, "public")));

app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/oglasi", listingsRouter);
// ...
export default app;

Your suggestion didn't work.

@IgorArnaut
Copy link
Author

The thing is that I don't want this data:

{
  "building": {
    "address": "Alekse Šantića 4",
    "constructedIn": "2009",
    "numOfFloors": "0",
    "hasParking": "on",
    "hasCctv": "on",
    "hasIntercom": "on"
  },
  "apartment": {
    "location": "Grbavica",
    "floor": "0",
    "area": "30",
    "price": "30",
    "numOfRooms": "0",
    "state": "Izvorno",
    "heating": "Gradsko",
    "equipment": "Namešten",
    "items": [
      "1",
      "2",
      "4",
      "6",
      "16"
    ]
  },
  "terms": {
    "availableOn": "2025-05-01",
    "hasDeposit": "on",
    "isForStudents": "on"
  },
  "listing": {
    "title": "Naslov",
    "description": "Opis."
  }
}

But this:

{
  "building": {
    "address": "Alekse Šantića 4",
    "constructedIn": 2009,
    "numOfFloors": 0,
    "hasParking": true,
    "hasGarage": false,
    "hasElevator": false,
    "hasCctv": true,
    "hasIntercom": true
  },
  "apartment": {
    "location": "Grbavica",
    "floor": 0,
    "area": 30,
    "price": 30,
    "numOfRooms": 0,
    "state": "Izvorno",
    "heating": "Gradsko",
    "equipment": "Namešten",
    "items": [
      "1",
      "2",
      "4",
      "6",
      "16"
    ]
  },
  "terms": {
    "availableOn": "2025-05-01",
    "hasDeposit": true,
    "isForStudents": true,
    "isForWorkers": false,
    "isSmokingAllowed": false,
    "arePetsAllowed": false
  },
  "listing": {
    "title": "Naslov",
    "description": "Opis."
  }
}

So I can use the form data object in the route with Prisma:

router.post("/postavka", async (req, res, next) => {
  console.log(req.body);
  let {
    building: buildingData,
    apartment: apartmentData,
    terms: temrsData,
    listing: listingData,
  } = req.body;

  buildingData = trueParse(buildingData);
  apartmentData = trueParse(apartmentData);
  temrsData = trueParse(temrsData);
  console.log(buildingData);

  let building = await prisma.building.findFirst({ where: buildingData });

  if (!building)
    building = await prisma.building.create({ data: buildingData });

  res.status(200).json({
    building: buildingData,
    apartment: apartmentData,
    terms: temrsData,
    listing: listingData,
  });
});

@krzysdz
Copy link
Contributor

krzysdz commented May 18, 2025

Unless you have special client-side JS code to change it, HTML <form>s can be sent as:

Neither of those encodings preserves information about data type (number, string, bool, Date), so effectively everything is a string. One exception is multipart/form-data, which sends a bit more information about files.

Checkboxes and radio buttons that are not checked are not sent. Checked ones are sent with value being their value attribute if it is specified or the string "on".

This has nothing to do with Express. It's just how HTML forms work.

You have a couple options to solve your problems:

  • validate and parse everything from strings by yourself,
  • find an existing library that will do it for you given a schema (yup should be able to do this),
  • try to send the from e.g. as a JSON formatted to your liking from the client side.

@bjohansebas bjohansebas removed the bug label May 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants