packages/docs/content/basics.mdx
import { Tabs, Tab } from 'fumadocs-ui/components/tabs'; import { Callout } from "fumadocs-ui/components/callout";
This page will walk you through the basics of creating schemas, parsing data, and using inferred types. For complete documentation on Zod's schema API, refer to Defining schemas.
Before you can do anything else, you need to define a schema. For the purposes of this guide, we'll use a simple object schema.
<Tabs groupId="lib" items={["Zod", "Zod Mini"]}> <Tab value="Zod">
import * as z from "zod";
const Player = z.object({
username: z.string(),
xp: z.number()
});
const Player = z.object({ username: z.string(), xp: z.number() });
</Tab>
</Tabs>
## Parsing data
Given any Zod schema, use `.parse` to validate an input. If it's valid, Zod returns a strongly-typed *deep clone* of the input.
```ts
Player.parse({ username: "billie", xp: 100 });
// => returns { username: "billie", xp: 100 }
await Player.parseAsync({ username: "billie", xp: 100 });
When validation fails, the .parse() method will throw a ZodError instance with granular information about the validation issues.
<Tabs groupId="lib" items={["Zod", "Zod Mini"]}> <Tab value="Zod">
try {
Player.parse({ username: 42, xp: "100" });
} catch(error){
if(error instanceof z.ZodError){
error.issues;
/* [
{
expected: 'string',
code: 'invalid_type',
path: [ 'username' ],
message: 'Invalid input: expected string'
},
{
expected: 'number',
code: 'invalid_type',
path: [ 'xp' ],
message: 'Invalid input: expected number'
}
] */
}
}
To avoid a try/catch block, you can use the .safeParse() method to get back a plain result object containing either the successfully parsed data or a ZodError. The result type is a discriminated union, so you can handle both cases conveniently.
const result = Player.safeParse({ username: 42, xp: "100" });
if (!result.success) {
result.error; // ZodError instance
} else {
result.data; // { username: string; xp: number }
}
await schema.safeParseAsync("hello");
When you only need to know whether an input is acceptable, use the top-level z.validate() function. It returns a boolean, never builds an error, and acts as a type guard on the schema's input type.
z.validate(Player, { username: "billie", xp: 100 }); // true
z.validate(Player, { username: 42, xp: "100" }); // false
Use z.validateAsync() for schemas with async refinements or transforms. On a compiled schema, z.validate() answers directly from the compiled fast path.
Zod infers a static type from your schema definitions. You can extract this type with the z.infer<> utility and use it however you like.
const Player = z.object({
username: z.string(),
xp: z.number()
});
// extract the inferred type
type Player = z.infer<typeof Player>;
// use it in your code
const player: Player = { username: "billie", xp: 100 };
In some cases, the input & output types of a schema can diverge. For instance, the .transform() API can convert the input from one type to another. In these cases, you can extract the input and output types independently:
const mySchema = z.string().transform((val) => val.length);
type MySchemaIn = z.input<typeof mySchema>;
// => string
type MySchemaOut = z.output<typeof mySchema>; // equivalent to z.infer<typeof mySchema>
// number
Sometimes the type comes first: a database model, a type from a generated client, an interface you don't own. Pass it to z.toZod<T>() and TypeScript checks that the schema's output type is exactly T.
type Player = {
username: string;
xp: number;
};
const Player = z.toZod<Player>()(
z.object({
username: z.string(),
xp: z.number(),
})
);
Player.shape.username; // ZodString — the schema is returned unchanged
The check is exact type equality, so any drift from the type is a compile error:
z.toZod<Player>()(
z.object({
username: z.string(),
xp: z.number(),
admin: z.boolean(), // ❌ extra key
})
);
The usual alternative is satisfies z.ZodType<Player>, which only checks assignability. It catches a missing required key, but extra keys, omitted optional keys, and a bare z.any() all slip through:
z.object({
username: z.string(),
xp: z.number(),
admin: z.boolean(),
}) satisfies z.ZodType<Player>; // ✅ no error
z.any() satisfies z.ZodType<Player>; // ✅ no error
Exactness applies to the type as TypeScript wrote it, so an intersection target matches .and() rather than .safeExtend():
type Entry = { id: string } & { label: string };
z.toZod<Entry>()(z.object({ id: z.string() }).and(z.object({ label: z.string() }))); // ✅
z.toZod<Entry>()(z.object({ id: z.string() }).safeExtend({ label: z.string() })); // ❌
Flatten the target and .safeExtend() matches instead:
type Flatten<T> = { [K in keyof T]: T[K] } & {};
z.toZod<Flatten<Entry>>()(z.object({ id: z.string() }).safeExtend({ label: z.string() })); // ✅
Now that we have the basics covered, let's jump into the Schema API.