Skip to content
rest-rpc
Esc
navigateopen⌘Jpreview
On this page

HTTP and Schema I/O gotchas

Avoid common HTTP wire format and schema input/output mistakes.

rest-rpc validates schemas at the server boundary, but it cannot change how HTTP transport formats or schema input/output work.

Default request and response bodies use JSON. WebSocket messages use JSON. Params, query, and headers use HTTP strings.

Request Serialization

✅ Good

request: {
	body: z.object({
		createdAt: z.string().datetime().transform((value) => new Date(value)),
	}),
}

Client sends a string. The handler receives a Date.

❌ Bad

request: {
	body: z.object({
		createdAt: z.date(),
	}),
}

Client sends a Date. JSON serialization converts it to a string. Validation fails because it expects a Date but receives a string.

Request Params and Query Serialization

✅ Good

request: {
	params: {
		id: z.coerce.number(),
	},
	query: {
		published: z.enum(["true", "false"]).transform((value) => value === "true"),
	},
}

Client can send a number or string for id and a string for published. The handler receives a number and a boolean.

❌ Bad

request: {
	params: {
		id: z.number(),
	},
	query: {
		published: z.boolean(),
	},
}

Client can send a number for id and a boolean for published. URL path params and query parameters are always strings. Validation fails because it expects a number and a boolean but receives strings.

Response Serialization

✅ Good

responses: {
	200: z.object({
		id: z.string(),
		createdAt: z.date().transform((value) => value.toISOString()),
	}),
}

Server sends a string. The client receives a valid date string and can transform it to a Date if needed outside of the client.

❌ Bad

responses: {
	200: z.object({
		id: z.string(),
		createdAt: z.date(),
	}),
}

Server sends a Date. JSON serialization converts it to a string. If client enables response validation, it fails because it expects a Date but receives a string. If the client does not enable response validation, it receives a string instead of a Date but TypeScript type will incorrectly indicate that it is a Date. This can lead to runtime errors when the client code tries to use the value as a Date.

Response Transforms

✅ Good (but with caveats)

responses: {
	200: z.object({
		id: z.string(),
		name: z.object({
			first: z.string(),
			last: z.string(),
		}).transform(({ first, last }) => `${first} ${last}`),
	}),
}

Server handler can return data in a different shape than the client receives. The transform is applied on the server before serialization.

However, if the client enables response validation, it will fail validation because the runtime code will try to validate the transformed output against its original schema.

❓ Confusing

responses: {
	200: z.object({
		id: z.string(),
		createdAt: z.iso.datetime().transform((value) => new Date(value)),
	}),
}

Server handler returns a string. Server applies the transform and sends a Date. JSON serialization converts it to a string. If the client enables response validation, the serialized string is transformed to a Date which will work fine. If the client does not enable response validation, it receives a string instead of a Date but TypeScript type will incorrectly indicate that it is a Date. In both cases, generated OpenAPI output can be misleading if the response schema output is not a JSON shape.

WebSocket Messages

WebSocket messages have the same schema input/output and JSON serialization gotchas as request and response bodies.

✅ Good

messages: {
	client: z.object({
		createdAt: z.string().datetime().transform((value) => new Date(value)),
	}),
	server: z.object({
		createdAt: z.date().transform((value) => value.toISOString()),
	}),
}

Client messages arrive as JSON and are validated on the server. Server messages are validated on the server before being sent as JSON.

❌ Bad

messages: {
	client: z.object({
		createdAt: z.date(),
	}),
	server: z.object({
		createdAt: z.date(),
	}),
}

The client message Date becomes a string before the server validates it. The server message Date becomes a string before the client receives it.

If the client enables validateResponses, incoming WebSocket server messages are validated on the client too. That can fail for transformed server message outputs that no longer match the original schema input.

Was this page helpful?