Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Default Values - Zagora
Skip to content

Default Values

Auto-fill missing arguments

Zagora automatically applies schema defaults at runtime, ensuring handlers always receive complete data.

How Defaults Work

When you define a schema with .default(), Zagora fills in missing values:

import { z } from 'zod';
import { zagora } from 'zagora';
 
const createPost = zagora()
  .input(z.object({
    title: z.string(),
    published: z.boolean().default(false),
    views: z.number().default(0)
  }))
  .handler((_, input) => {
    // input.published: boolean (not boolean | undefined!)
    // input.views: number (not number | undefined!)
    return input;
  })
  .callable();
 
createPost({ title: 'Hello' });
// { ok: true, data: { title: 'Hello', published: false, views: 0 } }

Defaults in Tuple Arguments

Defaults work seamlessly with tuple inputs:

const greet = zagora()
  .input(z.tuple([
    z.string(),                  // Required
    z.string().default('Hello'), // Default greeting
    z.number().default(1)        // Default count
  ]))
  .handler((_, name, greeting, count) => {
    // greeting: string (never undefined!)
    // count: number (never undefined!)
    return `${greeting} ${name}!`.repeat(count);
  })
  .callable();
 
greet('World');              // 'Hello World!'
greet('World', 'Hi');        // 'Hi World!'
greet('World', 'Hey', 3);    // 'Hey World!Hey World!Hey World!'

Default vs Optional

These behave differently:

// Default - value is guaranteed
z.number().default(0)
// Handler receives: number (never undefined)
 
// Optional - value may be undefined
z.number().optional()
// Handler receives: number | undefined

Choose defaults when you need a guaranteed value:

const paginate = zagora()
  .input(z.object({
    page: z.number().default(1),      // Always has a value
    limit: z.number().default(10),    // Always has a value
    filter: z.string().optional()     // May be undefined
  }))
  .handler((_, { page, limit, filter }) => {
    // No need to check if page/limit exist
    const offset = (page - 1) * limit;
    return { offset, limit, filter };
  })
  .callable();

Dynamic Defaults

Use functions for dynamic default values:

const createRecord = zagora()
  .input(z.object({
    name: z.string(),
    createdAt: z.date().default(() => new Date()),
    id: z.string().default(() => crypto.randomUUID())
  }))
  .handler((_, input) => input)
  .callable();
 
createRecord({ name: 'Test' });
// { name: 'Test', createdAt: Date, id: 'uuid-...' }

Nested Defaults

Defaults work at any nesting level:

const createUser = zagora()
  .input(z.object({
    name: z.string(),
    settings: z.object({
      theme: z.string().default('light'),
      notifications: z.object({
        email: z.boolean().default(true),
        push: z.boolean().default(false)
      }).default({})
    }).default({})
  }))
  .handler((_, input) => input)
  .callable();
 
createUser({ name: 'Alice' });
// {
//   name: 'Alice',
//   settings: {
//     theme: 'light',
//     notifications: { email: true, push: false }
//   }
// }

TypeScript Behavior

Zagora correctly infers non-undefined types for defaults:

const proc = zagora()
  .input(z.tuple([
    z.string(),
    z.number().default(10)
  ]))
  .handler((_, name, count) => {
    // name: string
    // count: number (NOT number | undefined)
    
    // TypeScript knows count is always defined
    return name.repeat(count);
  })
  .callable();

Defaults in Output Schemas

Defaults also work in output schemas:

const getUser = zagora()
  .input(z.string())
  .output(z.object({
    name: z.string(),
    role: z.string().default('user')
  }))
  .handler((_, id) => {
    // Can return without role, default applies
    return { name: 'Alice' };
  })
  .callable();

Next Steps