Overview
Parsing is the process of validating data against a schema. Zod provides multiple methods for parsing, each suited to different use cases:
parse() - Throws on validation failure
safeParse() - Returns result object
parseAsync() - Async version that throws
safeParseAsync() - Async version that returns result
parse()
The parse() method validates data and returns it if valid, otherwise throws a ZodError:
import { z } from 'zod';
const schema = z.string();
// Valid data - returns the value
const result = schema.parse('hello'); // 'hello'
// Invalid data - throws ZodError
try {
schema.parse(123);
} catch (error) {
if (error instanceof z.ZodError) {
console.log(error.issues);
}
}
From packages/zod/src/v4/classic/schemas.ts:195, parse() is defined as:parse(data: unknown, params?: ParseContext): Output
When to Use parse()
When you want validation errors to crash the request:
app.post('/users', (req, res) => {
const userData = UserSchema.parse(req.body);
// If we reach here, data is valid
const user = await createUser(userData);
res.json(user);
});
When you’re confident data is valid:
const config = ConfigSchema.parse(process.env);
// TypeScript knows config matches the schema
safeParse()
The safeParse() method never throws. Instead, it returns a result object:
const schema = z.number();
const result = schema.safeParse('not a number');
if (result.success) {
// Success case
console.log(result.data); // Typed as number
} else {
// Error case
console.log(result.error); // ZodError instance
}
Result Type
From packages/zod/src/v4/classic/parse.ts:4-6:
type ZodSafeParseResult<T> = ZodSafeParseSuccess<T> | ZodSafeParseError<T>;
type ZodSafeParseSuccess<T> = { success: true; data: T; error?: never };
type ZodSafeParseError<T> = { success: false; data?: never; error: ZodError<T> };
When to Use safeParse()
// User input validation
function validateUserInput(input: unknown) {
const result = UserSchema.safeParse(input);
if (!result.success) {
return {
errors: result.error.issues.map(issue => ({
path: issue.path,
message: issue.message,
})),
};
}
return { user: result.data };
}
// API responses
const apiResult = await fetch('/api/data');
const json = await apiResult.json();
const parsed = ApiResponseSchema.safeParse(json);
if (parsed.success) {
// Handle valid response
processData(parsed.data);
} else {
// Handle invalid response
logError('API returned invalid data', parsed.error);
}
parseAsync()
Use parseAsync() when schemas contain asynchronous validations (like .refine() with async functions):
const schema = z.string().refine(
async (email) => {
// Simulate async check (e.g., database lookup)
const exists = await checkEmailExists(email);
return !exists;
},
{ message: 'Email already exists' }
);
// Must use parseAsync for async refinements
try {
const email = await schema.parseAsync('[email protected]');
console.log('Valid email:', email);
} catch (error) {
console.error('Validation failed:', error);
}
Calling parse() on a schema with async refinements will throw an error. Always use parseAsync() for async validation.
const schema = z.number().transform(async (n) => {
// Async transformation
const result = await fetch(`/api/multiply?n=${n}`);
return result.json();
});
const data = await z.object({ id: schema }).parseAsync({ id: 5 });
// data.id is the transformed result
safeParseAsync()
Combines the safety of safeParse() with async support:
const schema = z.string().refine(
async (val) => {
const isValid = await validateWithExternalAPI(val);
return isValid;
},
'External validation failed'
);
const result = await schema.safeParseAsync('test-value');
if (result.success) {
console.log('Valid:', result.data);
} else {
console.error('Invalid:', result.error.issues);
}
Real-World Example
const RegistrationSchema = z.object({
username: z.string().min(3).refine(
async (username) => {
const exists = await db.users.findOne({ username });
return !exists;
},
{ message: 'Username already taken' }
),
email: z.string().email().refine(
async (email) => {
const exists = await db.users.findOne({ email });
return !exists;
},
{ message: 'Email already registered' }
),
password: z.string().min(8),
});
// In your route handler
app.post('/register', async (req, res) => {
const result = await RegistrationSchema.safeParseAsync(req.body);
if (!result.success) {
return res.status(400).json({
errors: result.error.issues,
});
}
const user = await createUser(result.data);
res.json({ user });
});
Parse Context
All parse methods accept an optional context parameter for customization:
const schema = z.string();
// Custom error messages
schema.parse('test', {
error: (issues) => {
return issues.map(issue => ({
...issue,
message: `Custom: ${issue.message}`,
}));
},
});
// Include input in errors
schema.safeParse('test', {
reportInput: true,
});
// Disable JIT optimization
schema.parse('test', {
jitless: true,
});
From packages/zod/src/v4/core/schemas.ts:16-25, the ParseContext interface:
interface ParseContext<T extends $ZodIssueBase = never> {
/** Customize error messages. */
readonly error?: $ZodErrorMap<T>;
/** Include the `input` field in issue objects. Default `false`. */
readonly reportInput?: boolean;
/** Skip eval-based fast path. Default `false`. */
readonly jitless?: boolean;
}
Error Handling Patterns
Pattern 1: Try-Catch with parse()
try {
const data = schema.parse(untrustedInput);
processData(data);
} catch (error) {
if (error instanceof z.ZodError) {
handleValidationError(error);
} else {
throw error; // Re-throw unexpected errors
}
}
Pattern 2: Conditional with safeParse()
const result = schema.safeParse(untrustedInput);
if (result.success) {
processData(result.data);
} else {
handleValidationError(result.error);
}
Pattern 3: Early Return
function processRequest(data: unknown) {
const parsed = RequestSchema.safeParse(data);
if (!parsed.success) {
return { error: parsed.error };
}
// TypeScript knows parsed.data is valid
return handleValidRequest(parsed.data);
}
Practical Examples
const FormSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
confirmPassword: z.string(),
}).refine(
(data) => data.password === data.confirmPassword,
{
message: 'Passwords do not match',
path: ['confirmPassword'],
}
);
function handleSubmit(formData: FormData) {
const result = FormSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
confirmPassword: formData.get('confirmPassword'),
});
if (!result.success) {
displayErrors(result.error.flatten());
return;
}
submitForm(result.data);
}
Environment Variables
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number).pipe(z.number().int().positive()),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
});
// Validate on startup
const env = EnvSchema.parse(process.env);
export default env;
API Request Validation
const CreateUserRequest = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
app.post('/users', async (req, res) => {
const result = CreateUserRequest.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
issues: result.error.issues,
});
}
const user = await createUser(result.data);
res.json(user);
});
Zod uses JIT (Just-In-Time) compilation for performance. The first parse is slower as it compiles the validator, but subsequent parses are much faster.
// First parse - compiles validator
const result1 = schema.parse(data); // ~1ms
// Subsequent parses - uses compiled validator
const result2 = schema.parse(data); // ~0.1ms
Disable JIT for debugging:
schema.parse(data, { jitless: true });
Best Practices
- Use
safeParse() for user input - Never trust external data
- Use
parse() for internal assertions - When data should always be valid
- Use async methods only when needed - Synchronous parsing is faster
- Handle errors gracefully - Provide clear error messages to users
- Validate early - Check data at system boundaries
Next Steps