Provide environment information
System:
OS: Linux 5.0 undefined
CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
Memory: 0 Bytes / 0 Bytes
Shell: 1.0 - /bin/jsh
Binaries:
Node: 20.19.1 - /usr/local/bin/node
Yarn: 1.22.19 - /usr/local/bin/yarn
npm: 10.8.2 - /usr/local/bin/npm
pnpm: 8.15.6 - /usr/local/bin/pnpm
npmPackages:
@tanstack/react-query: ^5.80.3 => 5.90.21
@trpc/client: canary => 11.9.1-canary.9+d92cc45b3
@trpc/next: canary => 11.9.1-canary.9+d92cc45b3
@trpc/react-query: canary => 11.9.1-canary.9+d92cc45b3
@trpc/server: canary => 11.9.1-canary.9+d92cc45b3
next: ^15.3.8 => 15.5.12
react: ^19.1.0 => 19.2.4
typescript: ^5.9.2 => 5.9.3
Describe the bug
I have written the following serializer/deserializer pair for use with httpLink. It allows serializing Blob objects contained inside of a plain object to be serialized as FormData fields. When you pass it an object { file: new Blob(["foo"]) }, it returns a formdata with field data set to {"file": <pointer to file 0>} and field blob set to new Blob(["foo"]).
If I pass this FormData object directly to a tRPC mutation, it gets correctly serialized. In this case I have to manually deserialize it on the server though. If I instead pass this function as a serializer to httpLink, the FormData returned from serializer is encoded as JSON (resulting in {} being the value sent). This obviously then breaks things.
Serializer/deserializer for Blob
import { parse, registerCustom, stringify } from "superjson";
let BLOBS: Blob[] = [];
registerCustom({
isApplicable: (v) => v instanceof Blob,
serialize: (blob) => {
BLOBS.push(blob);
return { index: BLOBS.length - 1 };
},
deserialize: (obj) => BLOBS[obj.index],
}, "Blob");
export function serialize(data: unknown): FormData | string {
BLOBS = [];
try {
const serialized = stringify(data);
if (BLOBS.length === 0) return serialized;
const formData = new FormData();
formData.append("data", serialized);
for (const blob of BLOBS) formData.append("file", blob);
console.log(formData);
return formData;
} finally {
BLOBS = [];
}
}
export function deserialize(val: FormData | string): unknown {
let serialized: string;
if (typeof val === "string") {
serialized = val;
BLOBS = [];
} else {
console.log(val);
const data = val.get("data");
if (typeof data !== "string") throw new Error("Expected 'data' field to be a string");
const files = val.getAll("file");
for (const file of files) {
if (!(file instanceof Blob)) throw new Error("Expected 'file' field to be a Blob");
}
serialized = data;
BLOBS = files as Blob[];
}
try {
return parse(serialized);
} finally {
BLOBS = [];
}
}
Link to reproduction
https://stackblitz.com/edit/trpc-serializing-formdata?file=src%2Fpages%2Findex.tsx
To reproduce
Press button in the demo above.
Additional information
My proposal is to allow serializer to return FormData, and to call deserializer even with FormData values from the wire. The latter is a breaking change, and would require a major version bump.
For now, I suggest a new option in the transformer called deserializeNonJsonTypes that defaults to false, but can be set to true to enable this behaviour.
For consistency, serializer should probable be called also for FormData inputs (right now it is not). This is again a breaking change, so should be flagged by a serializeNonJsonTypes option.
And furthermore I am saying FormData, but it should probably apply equally to all values that isNonJsonSerializable returns true for.
👨👧👦 Contributing
Provide environment information
Describe the bug
I have written the following serializer/deserializer pair for use with
httpLink. It allows serializingBlobobjects contained inside of a plain object to be serialized asFormDatafields. When you pass it an object{ file: new Blob(["foo"]) }, it returns a formdata with fielddataset to{"file": <pointer to file 0>}and fieldblobset tonew Blob(["foo"]).If I pass this FormData object directly to a tRPC mutation, it gets correctly serialized. In this case I have to manually deserialize it on the server though. If I instead pass this function as a
serializertohttpLink, theFormDatareturned fromserializeris encoded as JSON (resulting in{}being the value sent). This obviously then breaks things.Serializer/deserializer for Blob
Link to reproduction
https://stackblitz.com/edit/trpc-serializing-formdata?file=src%2Fpages%2Findex.tsx
To reproduce
Press button in the demo above.
Additional information
My proposal is to allow
serializerto returnFormData, and to calldeserializereven withFormDatavalues from the wire. The latter is a breaking change, and would require a major version bump.For now, I suggest a new option in the
transformercalleddeserializeNonJsonTypesthat defaults tofalse, but can be set totrueto enable this behaviour.For consistency,
serializershould probable be called also forFormDatainputs (right now it is not). This is again a breaking change, so should be flagged by aserializeNonJsonTypesoption.And furthermore I am saying
FormData, but it should probably apply equally to all values thatisNonJsonSerializablereturnstruefor.👨👧👦 Contributing