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#
- Prerequisites
- What is Zod?
- Creating a Basic Zod Schema
- Adding Default Values to Primitive Fields
- Creating a Primitive Object with Defaults
- Advanced Scenarios
- Best Practices
- Conclusion
- 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-nodefor 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),
}); Step 2: Infer the Type (Optional but Recommended)#
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 beundefinedin the input and output..default(): Replacesundefinedwith 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#
-
Use Functions for Dynamic Defaults
For values likeDateor random numbers, use a function in.default()to avoid stale values:z.date().default(() => new Date()); // Good z.number().default(() => Math.random()); // Good -
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"), }); -
Test Edge Cases
Validate behavior for:- Missing fields (should use defaults).
nullvs.undefined(defaults only apply toundefined).- Explicitly provided values (should override defaults).
-
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(notnullor 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.