What the error means
Zod validates a value against a schema and reports each mismatch with a path and a message. Expected number, received string (Zod 3) or Invalid input: expected number, received string (Zod 4) means the schema declared z.number() at that path and the input carried a string. No coercion is attempted by default: "2" is a string, and z.number() rejects it, exactly as TypeScript would reject assigning a string to a number.
In the example, page arrives as "2". A schema generated from a sample where page was 2 declares z.number() and fails on this input.
Why it happens
- The value came from a URL or a form. Query parameters, route parameters, and form fields are always strings, however numeric they look. This is the most common source.
- The API sends numbers as strings for IDs or money, to avoid precision loss in JavaScript clients.
- The generation sample had a number where the real input has a string.
- Environment variables, which are strings, validated against a schema that expects numbers.
How to fix it
- Decide what the value is. If it is a quantity you will do arithmetic on and it merely arrives as a string, coerce it:
z.coerce.number()converts"2"to2and still rejects"abc". This is the right tool for query strings and forms. - If it is an identifier — an order number, a SKU —
z.string()is the honest schema. Paste a sample where the value is a string into the generator above and it emitsz.string(). - If the producer is yours and the value should be a number on the wire, fix the producer; coercion at the consumer hides the inconsistency.
- For a field that is legitimately either,
z.union([z.number(), z.string()]), which the generator emits when samples disagree.
The schema for the example, with coercion where it belongs:
import { z } from "zod";
export const RootSchema = z.object({
page: z.coerce.number().int(),
limit: z.number().int(),
});
If it still fails
- Read the
pathin the issue:["items", 3, "price"]means the third element ofitems, and the coercion belongs on the element schema. z.coerce.number()turns""into0andnullinto0, which may not be what you want; add.min(1)or validate emptiness first.- The same mismatch in Pydantic reads
Input should be a valid integerand in Gocannot unmarshal string into Go struct field; JSON to TypeScript shows the permissive typing of the same data.
Related errors
Unrecognized key(s) in object
Add the key to the schema (regenerate from a fuller sample), or drop .strict() so unknown keys are stripped (default) or kept with .passthrough().
Input should be a valid integer
Match the annotation to the data: float for fractional values, str for codes, Optional[int] = None for nulls; use a validator or strict=False only when coercion is intended.