Zod: How to Create a Primitive Object with Default Values from a Schema

In modern TypeScript development, ensuring type safety and data validation is critical for building robust applications. Zod, a TypeScript-first schema declaration and validation library, has emerged as a popular choice for this task. Beyond validation, Zod simplifies data transformation—including setting default values for missing fields.

If you’ve ever needed to create an object where missing properties are automatically populated with sensible defaults (e.g., a user profile with a default "Guest" name or a form with pre-filled values), Zod’s default() method is your solution. In this guide, we’ll explore how to define Zod schemas for primitive types (strings, numbers, booleans, etc.), add default values, and combine them into a "primitive object" (an object with primitive-valued properties) that auto-populates missing fields with defaults.

Table of Contents#

  1. Prerequisites
  2. What is Zod?
  3. Creating a Basic Zod Schema
  4. Adding Default Values to Primitive Fields
  5. Creating a Primitive Object with Defaults
  6. Advanced Scenarios
  7. Best Practices
  8. Conclusion
  9. References

Prerequisites#

To follow along, you’ll need:

  • Basic familiarity with TypeScript.
  • Node.js (v14+ recommended) and npm/yarn installed.
  • A project set up with TypeScript (or use ts-node for quick testing).

Install Zod first:

npm install zod  
# or  
yarn add zod  

What is Zod?#

Zod is a TypeScript-first library for defining schemas to validate and transform data. It allows you to:

  • Declare schemas for primitives, objects, arrays, and more.
  • Validate input data against these schemas (throwing errors for invalid data).
  • Infer TypeScript types directly from schemas (no need to duplicate type definitions!).
  • Transform data (e.g., coerce strings to numbers, set default values for missing fields).

Zod’s power lies in its simplicity and tight integration with TypeScript, making it ideal for validating API inputs, form data, or configuration objects.

Creating a Basic Zod Schema#

Before adding defaults, let’s review how to define basic Zod schemas for primitives and objects.

Primitive Schemas#

Zod provides built-in schemas for all JavaScript primitives:

import { z } from "zod";  
 
// String schema  
const stringSchema = z.string();  
 
// Number schema  
const numberSchema = z.number();  
 
// Boolean schema  
const booleanSchema = z.boolean();  

Object Schemas#

Use z.object() to define object schemas, with keys mapped to primitive or complex schemas:

const userSchema = z.object({  
  name: z.string(), // Required string field  
  age: z.number(),  // Required number field  
  isActive: z.boolean(), // Required boolean field  
});  

Validating Data#

Use the .parse() method to validate input against a schema. Zod throws an error if the input is invalid:

// Valid input  
const validUser = userSchema.parse({  
  name: "Alice",  
  age: 30,  
  isActive: true,  
});  
 
// Invalid input (missing "age")  
try {  
  userSchema.parse({ name: "Bob", isActive: false });  
} catch (error) {  
  console.error("Validation failed:", error); // Throws error: "age is required"  
}  

Type Inference#

Zod automatically infers TypeScript types from schemas using z.infer<typeof Schema>:

type User = z.infer<typeof userSchema>;  
// Type User = { name: string; age: number; isActive: boolean }  

Adding Default Values to Primitive Fields#

The key to adding default values in Zod is the .default() method. It specifies a value to use when the input is undefined. Defaults are not applied if the input is null, 0, or an empty string—only when the field is explicitly undefined (or missing entirely).

Example 1: String with Default#

Define a string schema with a default value for when the input is undefined:

const nameSchema = z.string().default("Guest");  
 
// Input is undefined → use default  
nameSchema.parse(undefined); // "Guest"  
 
// Input is provided → use input  
nameSchema.parse("Alice"); // "Alice"  
 
// Input is null → throws error (null ≠ string)  
nameSchema.parse(null); // ZodError: Invalid input  

Example 2: Number with Default#

Similarly, add a default to a number schema:

const ageSchema = z.number().default(18);  
 
ageSchema.parse(undefined); // 18 (default used)  
ageSchema.parse(25); // 25 (input used)  

Example 3: Boolean with Default#

Booleans can also have defaults (e.g., default to true for "active" flags):

const isActiveSchema = z.boolean().default(true);  
 
isActiveSchema.parse(undefined); // true (default used)  
isActiveSchema.parse(false); // false (input used)  

Creating a Primitive Object with Defaults#

Now, combine primitive schemas with defaults into an object schema using z.object(). This creates a "primitive object" (all fields are primitives) where missing fields are auto-populated with defaults.

Step 1: Define the Object Schema with Defaults#

Use z.object() and chain .default() to each primitive field:

import { z } from "zod";  
 
const userWithDefaultsSchema = z.object({  
  // String with default "Guest"  
  name: z.string().default("Guest"),  
 
  // Number with default 18  
  age: z.number().default(18),  
 
  // Boolean with default true  
  isActive: z.boolean().default(true),  
});  

Infer the TypeScript type from the schema to leverage type safety:

type UserWithDefaults = z.infer<typeof userWithDefaultsSchema>;  
// Type: { name: string; age: number; isActive: boolean }  
// (All fields are required in the output, thanks to defaults!)  

Step 3: Parse Input with Missing Fields#

When parsing input, Zod applies defaults only for fields that are undefined (or missing).

Example 1: Empty Input#

Parse an empty object {}—all fields use defaults:

const emptyInput = {};  
const user1 = userWithDefaultsSchema.parse(emptyInput);  
 
console.log(user1);  
// Output: { name: "Guest", age: 18, isActive: true }  

Example 2: Partial Input#

Parse input with some fields provided—missing fields use defaults:

const partialInput = { name: "Bob", age: 30 };  
const user2 = userWithDefaultsSchema.parse(partialInput);  
 
console.log(user2);  
// Output: { name: "Bob", age: 30, isActive: true }  
// (isActive uses default since it's missing)  

Example 3: Input with undefined Fields#

Explicitly setting a field to undefined also triggers the default:

const inputWithUndefined = { name: undefined, age: 22 };  
const user3 = userWithDefaultsSchema.parse(inputWithUndefined);  
 
console.log(user3);  
// Output: { name: "Guest", age: 22, isActive: true }  
// (name uses default because input is undefined)  

Key Note: null vs. undefined#

Zod only applies defaults when the input is undefined. If a field is null, Zod throws an error (unless you explicitly allow null with .nullable()):

// ❌ Error: "name" must be a string (null is invalid)  
userWithDefaultsSchema.parse({ name: null });  
 
// ✅ Allow null with .nullable(), then apply default if undefined  
const nullableNameSchema = z.string().nullable().default("Guest");  
nullableNameSchema.parse(null); // "Guest" (null is allowed, but default is only for undefined)  
nullableNameSchema.parse(undefined); // "Guest" (default used)  

Advanced Scenarios#

Nested Objects with Defaults#

Zod supports nested objects—you can add defaults to nested fields too!

Example: User with a Nested Address
Define an address schema with defaults, then nest it in the user schema:

// Define nested address schema with defaults  
const addressSchema = z.object({  
  city: z.string().default("New York"),  
  zipCode: z.string().default("10001"),  
});  
 
// User schema with nested address (and its own defaults)  
const userWithAddressSchema = z.object({  
  name: z.string().default("Guest"),  
  address: addressSchema.default({}), // Default to empty object (then addressSchema applies its defaults)  
});  
 
// Parse empty input → all defaults applied  
const userWithAddress = userWithAddressSchema.parse({});  
console.log(userWithAddress);  
// Output:  
// {  
//   name: "Guest",  
//   address: { city: "New York", zipCode: "10001" }  
// }  

Dynamic Defaults (e.g., Dates)#

For dynamic values like Date objects or random numbers, use a function with .default(). This ensures the default is generated fresh on each parse (instead of once when the schema is defined).

Bad Practice: Static Date

// ❌ Date is created once when the schema is defined (stale after schema initialization)  
const badDateSchema = z.date().default(new Date());  

Good Practice: Dynamic Date (Function)

// ✅ Date is created fresh on each parse  
const goodDateSchema = z.date().default(() => new Date());  
 
// Parse later → gets current date  
setTimeout(() => {  
  console.log(goodDateSchema.parse(undefined)); // Date at time of parse (not schema creation)  
}, 1000);  

Optional vs. Default Values#

Zod has two ways to handle missing fields:

  • .optional(): Allows the field to be undefined in the input and output.
  • .default(): Replaces undefined with a default value (field is required in the output).

Example: Optional vs. Default

const optionalSchema = z.object({  
  optionalField: z.string().optional(), // Output: string | undefined  
});  
 
const defaultSchema = z.object({  
  defaultField: z.string().default("Guest"), // Output: string (never undefined)  
});  
 
optionalSchema.parse({}); // { optionalField: undefined }  
defaultSchema.parse({}); // { defaultField: "Guest" }  

Best Practices#

  1. Use Functions for Dynamic Defaults
    For values like Date or random numbers, use a function in .default() to avoid stale values:

    z.date().default(() => new Date()); // Good  
    z.number().default(() => Math.random()); // Good  
  2. Document Defaults
    Explicitly document default values in comments for clarity:

    const userSchema = z.object({  
      // Default: "Guest" (used when name is undefined)  
      name: z.string().default("Guest"),  
    });  
  3. Test Edge Cases
    Validate behavior for:

    • Missing fields (should use defaults).
    • null vs. undefined (defaults only apply to undefined).
    • Explicitly provided values (should override defaults).
  4. Avoid Mutable Defaults
    Never use mutable objects (e.g., [], {}) as static defaults—they can lead to unexpected side effects. Use functions instead:

    // ❌ Mutable default (shared across all parses)  
    z.array(z.string()).default([]);  
     
    // ✅ Fresh array on each parse  
    z.array(z.string()).default(() => []);  

Conclusion#

Zod’s .default() method simplifies creating primitive objects with default values. By combining z.object() with primitive schemas and .default(), you can ensure missing fields are auto-populated with sensible defaults—no manual checks or fallback logic required!

Key takeaways:

  • Use .default(value) to set static defaults for primitives.
  • Use .default(() => value) for dynamic defaults (e.g., dates).
  • Defaults apply only when input is undefined (not null or other values).
  • Nested objects can also have defaults (nest schemas and use .default({}) for parent fields).

Zod’s approach keeps your code type-safe, concise, and easy to maintain—perfect for validating API responses, form data, or configuration files.

References#