🔧 Error Fixes
· 2 min read
Last updated on

tRPC: Input Validation Error — How to Fix It


TRPCError: Input validation failed
  [
    {
      "code": "invalid_type",
      "expected": "string",
      "received": "number",
      "path": ["id"],
      "message": "Expected string, received number"
    }
  ]

This error means the data you’re sending to a tRPC procedure doesn’t match the Zod schema defined in .input(). tRPC validates all inputs at runtime before your procedure code runs.

What causes this

tRPC uses Zod (or another validator) to validate procedure inputs. When the client sends data that doesn’t match the schema — wrong type, missing field, invalid format — tRPC rejects the request with a BAD_REQUEST error before your handler ever executes.

Common triggers:

  • Sending a number where a string is expected (or vice versa)
  • Missing required fields in the input object
  • Sending undefined or null for a non-optional field
  • URL params or form data arriving as strings when the schema expects numbers

Fix 1: Match your client input to the schema

Check the server schema and make sure your client sends exactly what it expects:

// Server — expects a UUID string
export const appRouter = router({
  getUser: publicProcedure
    .input(z.object({ id: z.string().uuid() }))
    .query(({ input }) => getUser(input.id)),
});

// ❌ Wrong — sending a number
trpc.getUser.useQuery({ id: 123 });

// ❌ Wrong — not a valid UUID
trpc.getUser.useQuery({ id: "not-a-uuid" });

// ✅ Correct — valid UUID string
trpc.getUser.useQuery({ id: "550e8400-e29b-41d4-a716-446655440000" });

Fix 2: Make fields optional where appropriate

If a field isn’t always required, update the schema:

.input(z.object({
  name: z.string(),
  email: z.string().email().optional(),    // Can be undefined
  bio: z.string().default(""),             // Defaults to empty string
  age: z.number().nullable(),              // Can be null
}))

Use .optional() when the field might not be sent at all. Use .nullable() when the field is sent but can be null. Use .default() to provide a fallback value.

Fix 3: Coerce types from URL params or forms

Data from URL search params and form submissions arrives as strings. Use Zod’s .coerce to convert automatically:

.input(z.object({
  page: z.coerce.number().int().positive(),  // "3" → 3
  active: z.coerce.boolean(),                // "true" → true
}))

This is especially common when building tRPC procedures that receive data from URL query parameters.

Fix 4: Debug the actual input

If you’re not sure what’s being sent, log the raw input before validation:

const myProcedure = publicProcedure
  .input(z.object({ id: z.string() }))
  .query(({ input, ctx }) => {
    console.log("Received input:", input);
    return getUser(input.id);
  });

On the client side, check what React Query is sending by inspecting the network tab — look at the request URL or body for the actual payload.

How to prevent it

  • Use tRPC’s end-to-end type safety. If you’re using @trpc/react-query, the TypeScript compiler will flag type mismatches at build time — but only if you don’t use as any or ignore type errors.
  • Keep your Zod schemas as the single source of truth. Infer your TypeScript types from them with z.infer<typeof schema> instead of defining types separately.
  • Add .describe() to your Zod fields to document what each field expects — it helps when debugging validation errors.