Computed Default Values in Zod

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

  1. Use transform for derived data: Computed defaults belong in transform, not in individual field definitions
  2. Keep transforms pure: Don’t cause side effects; just transform the data
  3. Type your transform output: Explicitly type the result to catch errors early
  4. 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

  1. transform is the right tool for computed defaults
  2. Falls back gracefully when dependent fields are missing
  3. Type-safe: Full TypeScript inference throughout
  4. Composable: Chain multiple transforms for complex logic
  5. Declarative: Schema clearly shows dependencies

πŸ“š Resources