Blog
Wild & Free Tools

tRPC + Zod Input Validation: Schema Setup Guide

Last updated: April 2026 6 min read

Table of Contents

  1. How tRPC Uses Zod
  2. Generating Schemas from Real Payloads
  3. Mutation Schema Pattern
  4. Sharing Schemas Between Client and Server
  5. Frequently Asked Questions

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:

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 Converter

Frequently 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.

Launch Your Own Clothing Brand — No Inventory, No Risk