tRPC + Zod Input Validation: Schema Setup Guide
Table of Contents
tRPC uses Zod for input validation natively — every tRPC procedure with an .input() call accepts a Zod schema, and tRPC validates the input automatically before your handler runs. You define the schema once and get TypeScript types on both client and server for free.
This guide shows the setup, how to generate schemas from real payloads, and common patterns for queries and mutations.
How tRPC Uses Zod
tRPC has native Zod support. Pass any Zod schema to .input() and tRPC will:
- Validate the input at runtime before calling your handler
- Return a typed error if validation fails
- Infer the input type on the client automatically — no manual type sharing needed
import { z } from 'zod';
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: z.number().int().positive() }))
.query(({ input }) => {
// input is typed as { id: number }
return db.users.findById(input.id);
})
});
On the client, calling trpc.getUser.query({ id: 1 }) is fully typed — TypeScript knows the input must be { id: number }.
Generating Schemas from Real Payloads
For complex inputs, generate the base schema from a real payload sample instead of writing it by hand. Paste the payload JSON into the free JSON to Zod converter:
{
"userId": 42,
"title": "My Post",
"content": "Hello world",
"tags": ["typescript", "trpc"],
"published": false
}
Generated schema:
const schema = z.object({
userId: z.number(),
title: z.string(),
content: z.string(),
tags: z.array(z.string()),
published: z.boolean()
});
Then add tRPC-appropriate constraints:
const CreatePostInput = z.object({
userId: z.number().int().positive(),
title: z.string().min(1).max(200),
content: z.string().min(1),
tags: z.array(z.string()).max(10).optional(),
published: z.boolean().default(false)
});
Sell Custom Apparel — We Handle Printing & Free Shipping
Mutation Schema Pattern
export const appRouter = t.router({
createPost: t.procedure
.input(CreatePostInput)
.mutation(async ({ input }) => {
// input is fully typed from the schema
const post = await db.posts.create({
data: {
title: input.title,
content: input.content,
userId: input.userId,
published: input.published
}
});
return post;
}),
updatePost: t.procedure
.input(z.object({
id: z.number(),
data: CreatePostInput.partial() // all fields optional for updates
}))
.mutation(async ({ input }) => {
return db.posts.update({
where: { id: input.id },
data: input.data
});
})
});
Notice CreatePostInput.partial() — Zod's .partial() makes all fields optional, perfect for update payloads where only changed fields are sent.
Sharing Schemas Between Client and Server
One of tRPC's biggest advantages is that the schema definition on the server automatically flows to the client as types. But you can also share schemas explicitly for client-side form validation:
// shared/schemas/post.ts
import { z } from 'zod';
export const CreatePostInput = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
published: z.boolean().default(false)
});
export type CreatePostData = z.infer<typeof CreatePostInput>;
// Server: use in tRPC procedure
import { CreatePostInput } from '~/shared/schemas/post';
.input(CreatePostInput)
// Client: use with React Hook Form
import { CreatePostInput, CreatePostData } from '~/shared/schemas/post';
const form = useForm<CreatePostData>({ resolver: zodResolver(CreatePostInput) });
One schema definition validates form input on the client AND the procedure input on the server.
Try It Free — No Signup Required
Runs 100% in your browser. No data is collected, stored, or sent anywhere.
Open Free JSON to Zod ConverterFrequently Asked Questions
Does tRPC require Zod for input validation?
No, tRPC supports other validators (Yup, Superstruct, Valibot) via adapters. But Zod is the officially recommended and most commonly used option, with the best TypeScript integration.
How do I generate a Zod schema for a tRPC procedure input?
Paste a sample input payload JSON into the free converter at /developer-tools/json-to-zod/. Copy the generated schema, add constraints like .min(), .max(), .optional(), and pass it to .input() in your tRPC router.
Can I reuse a tRPC input schema on the client for form validation?
Yes. Export the schema from a shared file. Import it in your tRPC router for server-side validation and in your React Hook Form component with zodResolver for client-side validation. One schema, both sides.
How does tRPC handle Zod validation errors?
If Zod validation fails, tRPC returns a TRPCError with code BAD_REQUEST and the ZodError details in the message. On the client, trpc catches this and the error is available in the mutation/query error state.

