Next.js Discord

Discord Forum

parse array from formData in server actions

Unanswered
Tamaskan Dog posted this in #help-forum
Open in Discord
Tamaskan DogOP
i am trying to implement a server action that recieves a bunch of data as FormData because it contains files so it has to be FormData ... the problem is that i can't parse the arrays from the FormData ... i tried to get the arrays by using FormData.getAll() but it returns undefined ... i tried to parse it by Object.fromEntries() same problem it parse everything but arrays

53 Replies

Tamaskan DogOP
export function buildFormData(formData: FormData, data: any, parentKey?: string): void {
  if (data && typeof data === 'object' && !(data instanceof Date) && !(data instanceof File)) {
    Object.keys(data).forEach((key) => {
      buildFormData(formData, data[key], parentKey ? `${parentKey}[${key}]` : key);
    });
  } else {
    const value = data == null ? '' : data;

    if (!parentKey) return;
    formData.append(parentKey, value);
  }
}
this is the payload sent with the POST
even when trying to use zsa library to parse the data
the array is not detected nor parsed
@Tamaskan Dog ts export function buildFormData(formData: FormData, data: any, parentKey?: string): void { if (data && typeof data === 'object' && !(data instanceof Date) && !(data instanceof File)) { Object.keys(data).forEach((key) => { buildFormData(formData, data[key], parentKey ? `${parentKey}[${key}]` : key); }); } else { const value = data == null ? '' : data; if (!parentKey) return; formData.append(parentKey, value); } }
this buildFormData function is buggy. i don't know what you are doing with that ${parentKey}[${key}] there but it's not compatible with formdata repeatable values.

just keep it simple:

// In the client:
const form = new FormData();
const textArray = ["foo", "bar", "baz"];
const numberArray = [1, 2, 3];

for (const value of textArray) {
  form.append("textArray", value);
}
for (const value of numberArray) {
  form.append("numberArray", value.toString());
}

// In the server:
import { z } from "zod";
import { zfd } from "zod-form-data";

const result = zfd
  .formData({
    textArray: zfd.repeatable(z.array(zfd.text())),
    numberArray: zfd.repeatable(z.array(zfd.numeric())),
  })
  .parse(form);
console.log(result);
// {
//   textArray: ["foo", "bar", "baz"],
//   numberArray: [1, 2, 3],
// }
Tamaskan DogOP
the parent key is for nested data cuz the used object is nasted with objects and arrays
from checking the zsa documentation it seems you can just use normal objects, it's not required to use formdata it looks like
then you can get rid of all that formdata handling code
but for form data, repeatable values are made by simply appending to the same key multiple times
form.append("foo", "first value")
form.append("foo", "second value")
form.append("foo", "third value")
form.append("foo", "fourth value")
@joulev from checking the zsa documentation it seems you can just use normal objects, it's not required to use formdata it looks like
Tamaskan DogOP
Error: Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported.
i got this error when trying to get rid of formData
@Tamaskan Dog Error: Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported.
which means inside your object there are some non-serialisable values. check this list for allowed values inside the object: https://react.dev/reference/rsc/use-server#serializable-parameters-and-return-values

server actions do accept objects but those objects must be serialisable to be sent over the network
i am checking the docs and it can only be used with formData
the action in the homepage https://zsa.vercel.app/docs/introduction literally accepts an object: { number: 24 }
{ number: 24 } is not a formdata value
either way, if you want to use formdata, you need to change your object->formdata conversion function
Tamaskan DogOP
true but when u want to pass a file it has to be FormData
"use client"

import { useActionState } from "zsa-react";
import { uploadFile } from "./actions";

export default function FileUploadExample() {
  const [[data, error], submitAction, isPending] = useActionState(uploadFile, [null, null]); 

  return (
    <form action={submitAction} className="flex flex-col gap-4">
      <label>
        Name:
        <input type="text" name="name" required />
      </label>
      <label>
        File:
        <input type="file" name="file" required />
      </label>
      <button type="submit" disabled={isPending}>
        Upload
      </button>
      {isPending && <div>Uploading...</div>}
      {data && <div>Success: {data}</div>}
      {error && <div>Error: {JSON.stringify(error.fieldErrors)}</div>}
    </form>
  );
}
yes that's true, with a file you need formdata
Tamaskan DogOP
i got multible files in my passes data
with a multible arrays
and nasted array of objects
that's why i am usring the [parentKey]
@joulev either way, if you want to use formdata, you need to change your object->formdata conversion function
and since you have to use formdata, you must update that function
which means you have to either update the server action handling mechanisms or update that function
multer parses the form in a different way compared to zsa
i don't know how multer and zsa work behind the scenes, so you need to check that
Tamaskan DogOP
what about an array of objects .... how can i append it to the formData
@joulev which means you have to either update the server action handling mechanisms or update that function
Tamaskan DogOP
i will try to update the function first but the array of objects is the problem now
i don't usually find myself in situations where i need to put an object inside formdata
Tamaskan DogOP
so i have to give on server actions and use route handler instead ?
cuz the data is too complex for a server action
this might suck, but you could just pass a stringified JSON onto a single field on the FormData, then parse it with zod on the endpoint, like what trpc did. just my 2 cents
hey that's an ingenious idea
props to you i completely did not think of this
yeah you could put the files as different fields
the stringified json only takes a single field so it shouldn't bother with the file attachments
Tamaskan DogOP
worked for the objects and the arrays .... but unfortunately i lost the attachments
* remove files from the object
* send files in separate fields and the non-file object in a separate field
* in the server, merge them back together after parsing
@Tamaskan Dog ts export function buildFormData(formData: FormData, data: any, parentKey?: string): void { if (data && typeof data === 'object' && !(data instanceof Date) && !(data instanceof File)) { Object.keys(data).forEach((key) => { buildFormData(formData, data[key], parentKey ? `${parentKey}[${key}]` : key); }); } else { const value = data == null ? '' : data; if (!parentKey) return; formData.append(parentKey, value); } }
Tamaskan DogOP
i found a walkthrough to parse the output of this function ... i think it simulate the approach used in multer
ts 
export default function parseFormData(formData: FormData): any {
  const object = {};
  const entries = formData.entries();
  for (const [key, value] of Array.from(entries)) {
    _.set(object, key, value);
  }
  return object;
}

it uses lodash to assign a deeply nested fields and arrays .... it worked for both the files and the nested arrays and objects
but i have to override the parsing function used in the zsa
@joulev * remove files from the object * send files in separate fields and the non-file object in a separate field * in the server, merge them back together after parsing
^^ passing file content in the JSON object would need a bit of a workaround like base64-ing it, I def wouldn't recommend that. You should put only the filenames of the attachments in the JSON object as reference to the files that will then be added to the form data FormData.

something like this:
const files = [...];
const formData = new FormData();

formData.put("data", JSON.stringify({ files: files.map((f) => f.nameOrSomething), ... }));
for (const f of files) {
  formData.put(f.nameOrSomething, f);
}

await theServerAction(formData);
Tamaskan DogOP
So either overriding the parsing function in the server side to parse the deeply nested object without losing the structure or stringifying the object and sending the files in separated fields then restructuring it in the server side