Computed Default Values in Zod
Use Zod's transform method to set dynamic default values based on other schema properties.
Computed Default Values in Zod
Zod provides powerful schema validation with support for default values. But what if a default value depends on another property? This guide shows how to handle computed defaults using Zodβs transform method.
π Simple Default Values
Basic default values in Zod are straightforward:
const schema = z.object({
name: z.string().default('John Doe'),
});
If name isnβt provided during parsing, it defaults to 'John Doe'.
π οΈ Computed Defaults: Depending on Other Properties
The real power comes when you need a default that depends on another field. Consider a blog schema where displayedTitle should match title, but with line breaks added for formatting:
const blogSchema = z
.object({
title: z.string(),
displayedTitle: z.string().optional(),
})
.transform(data => ({
...data,
displayedTitle: data.displayedTitle || data.title,
}));
Now, if displayedTitle isnβt provided, it falls back to title:
const blogData = {
title: 'Building Web Applications',
};
const parsed = blogSchema.parse(blogData);
// {
// title: 'Building Web Applications',
// displayedTitle: 'Building Web Applications'
// }
π Reverse Transformation: Deriving One Field from Another
You can also derive one field from another. For example, remove line breaks from displayedTitle to create title:
const blogSchema = z
.object({
displayedTitle: z.string(),
})
.transform(data => ({
...data,
title: data.displayedTitle.replace(/\n/g, ' '),
}));
const blogData = {
displayedTitle: 'Building Web\nApplications',
};
const parsed = blogSchema.parse(blogData);
// {
// displayedTitle: 'Building Web\nApplications',
// title: 'Building Web Applications'
// }
π‘ Advanced Pattern: Conditional Defaults
For more complex logic, combine multiple conditions:
const userSchema = z
.object({
firstName: z.string(),
lastName: z.string(),
displayName: z.string().optional(),
})
.transform(data => ({
...data,
displayName: data.displayName || `${data.firstName} ${data.lastName}`,
}));
const user = {
firstName: 'John',
lastName: 'Doe',
};
const parsed = userSchema.parse(user);
// {
// firstName: 'John',
// lastName: 'Doe',
// displayName: 'John Doe'
// }
π― Best Practices
- Use
transformfor derived data: Computed defaults belong intransform, not in individual field definitions - Keep transforms pure: Donβt cause side effects; just transform the data
- Type your transform output: Explicitly type the result to catch errors early
- Test edge cases: Verify behavior when fields are missing, empty, or null
π Complete Example
import { z } from 'zod';
const productSchema = z
.object({
name: z.string(),
price: z.number().positive(),
slug: z.string().optional(),
description: z.string().optional(),
})
.transform(data => ({
...data,
// Generate slug from name if not provided
slug: data.slug || data.name.toLowerCase().replace(/\s+/g, '-'),
// Provide default description
description: data.description || `Learn about ${data.name}`,
}));
const product = {
name: 'TypeScript Handbook',
price: 29.99,
};
const parsed = productSchema.parse(product);
// {
// name: 'TypeScript Handbook',
// price: 29.99,
// slug: 'typescript-handbook',
// description: 'Learn about TypeScript Handbook'
// }
π‘ Key Takeaways
transformis the right tool for computed defaults- Falls back gracefully when dependent fields are missing
- Type-safe: Full TypeScript inference throughout
- Composable: Chain multiple transforms for complex logic
- Declarative: Schema clearly shows dependencies