Skip to content

Commit 83841b7

Browse files
committed
fix(core): preserve message payloads during validation
1 parent 86d7015 commit 83841b7

2 files changed

Lines changed: 55 additions & 4 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import { z as z3 } from "zod/v3";
3+
import { z as z4 } from "zod/v4";
4+
import { ZodMessageSender, sendMessageInCatalog } from "./zodMessageHandler.js";
5+
6+
const schemas = [
7+
[
8+
"Zod 3",
9+
z3.object({ value: z3.string().transform(Number), added: z3.string().default("default") }),
10+
],
11+
[
12+
"Zod 4",
13+
z4.object({ value: z4.string().transform(Number), added: z4.string().default("default") }),
14+
],
15+
] as const;
16+
17+
describe("Zod message sending", () => {
18+
it.each(schemas)("ZodMessageSender preserves the original %s payload", async (_name, schema) => {
19+
const catalog = { TEST: schema };
20+
const payload = { value: "42", extra: "preserved" };
21+
const sender = vi.fn(async (_message: unknown) => {});
22+
const messageSender = new ZodMessageSender({ schema: catalog, sender });
23+
24+
await messageSender.send("TEST", payload);
25+
26+
expect(sender).toHaveBeenCalledWith({ type: "TEST", payload, version: "v1" });
27+
});
28+
29+
it.each(schemas)(
30+
"sendMessageInCatalog preserves the original %s payload",
31+
async (_name, schema) => {
32+
const catalog = { TEST: schema };
33+
const payload = { value: "42", extra: "preserved" };
34+
const sender = vi.fn(async (_message: unknown) => {});
35+
36+
await sendMessageInCatalog(catalog, "TEST", payload, sender);
37+
38+
expect(sender).toHaveBeenCalledWith({ type: "TEST", payload, version: "v1" });
39+
}
40+
);
41+
});

packages/core/src/v3/zodMessageHandler.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,13 @@ export class ZodMessageSender<TMessageCatalog extends ZodMessageCatalogSchema> {
282282
}
283283

284284
try {
285-
// send the validated/normalized output so zod transforms, defaults, and coercions are applied
286-
await this.#sender({ type, payload: parsedPayload.data, version: "v1" });
285+
// Preserve the original payload on the wire for backwards compatibility. Validation is
286+
// intentionally side-effect-free here; receivers parse the payload into the schema output.
287+
await this.#sender({
288+
type,
289+
payload: payload as unknown as inferZodSchemaOutput<TMessageCatalog[keyof TMessageCatalog]>,
290+
version: "v1",
291+
});
287292
} catch (error) {
288293
console.error("[ZodMessageSender] Failed to send message", error);
289294
}
@@ -338,8 +343,13 @@ export async function sendMessageInCatalog<TMessageCatalog extends ZodMessageCat
338343
throw new ZodSchemaParsedError(parsedPayload.error, payload);
339344
}
340345

341-
// send the validated/normalized output so zod transforms, defaults, and coercions are applied
342-
await sender({ type, payload: parsedPayload.data, version: "v1" });
346+
// Preserve the original payload on the wire for backwards compatibility. Validation is
347+
// intentionally side-effect-free here; receivers parse the payload into the schema output.
348+
await sender({
349+
type,
350+
payload: payload as unknown as inferZodSchemaOutput<TMessageCatalog[keyof TMessageCatalog]>,
351+
version: "v1",
352+
});
343353
}
344354

345355
export type MessageCatalogToSocketIoEvents<TCatalog extends ZodMessageCatalogSchema> = {

0 commit comments

Comments
 (0)