| name | zod |
| description | Use when authoring or modifying Zod schemas -- composition, metadata, and validation patterns |
Comprehensive guide for authoring and modifying Zod schemas in the schemas package, covering composition, metadata, validation patterns, and project conventions.
Core Rule: z.strictObject()
Always use z.strictObject() instead of z.object(). Strict objects reject unknown properties, which is critical for schema validation:
const Schema = z.strictObject({
name: z.string().meta({ description: 'The display name' }),
});
const Schema = z.object({
name: z.string().meta({ description: 'The display name' }),
});
Metadata: .meta() on Every Field
Every field MUST have .meta() with at least a description. This is used to generate JSON Schema documentation.
const DataSchema = z.strictObject({
document_id: z
.string()
.uuid()
.meta({
description: 'Unique identifier for the document',
examples: ['550e8400-e29b-41d4-a716-446655440000'],
}),
weight_kg: z
.number()
.positive()
.meta({
description: 'Weight of the material in kilograms',
examples: [150.5],
}),
notes: z
.string()
.optional()
.meta({
description: 'Additional notes about the document',
examples: ['Verified by field inspector'],
}),
status: z.enum(['pending', 'approved', 'rejected']).meta({
description: 'Current processing status',
examples: ['pending'],
}),
});
Parent objects also need .meta():
const Schema = z
.strictObject({ ... })
.meta({
description: 'Data payload for the credit audit schema',
});
Schema Composition Layers
The schemas package uses a layered composition pattern:
BaseIpfsSchema (common IPFS fields: name, description, image, external_url)
└── NftIpfsSchema (adds NFT fields: attributes, data)
└── TypeSchema (type-specific data and attributes)
Extending with .safeExtend()
Always use .safeExtend() to compose schemas. This preserves strict object validation:
import { NftIpfsSchema } from '../shared/nft-ipfs.schema';
export const CreditAuditSchema = NftIpfsSchema.safeExtend({
data: CreditAuditDataSchema.meta({
description: 'Credit audit specific data',
}),
}).meta({
description: 'Credit Audit IPFS schema for NFT metadata',
});
Why .safeExtend() and not .extend(): While both preserve strictObject enforcement, .extend() throws at runtime on schemas with refinements and clears existing checks. .safeExtend() preserves refinements and provides stricter TypeScript types that prevent incompatible field overrides.
Field Type Patterns
Strings
name: z.string().meta({ description: 'Display name' }),
document_id: z.string().uuid().meta({ description: 'Document UUID' }),
external_url: z.string().url().meta({ description: 'External URL' }),
code: z.string().min(1).max(50).meta({ description: 'Short code' }),
hash: z.string().regex(/^0x[a-fA-F0-9]{64}$/).meta({ description: 'Hex hash' }),
Numbers
weight: z.number().positive().meta({ description: 'Weight in kg' }),
count: z.number().int().nonneg().meta({ description: 'Item count' }),
percentage: z.number().min(0).max(100).meta({ description: 'Percentage value' }),
Enums
status: z
.enum(['pending', 'approved', 'rejected'])
.meta({ description: 'Processing status' }),
category: z
.enum(['Recycling Center', 'Collection Point', 'Processing Facility'])
.meta({ description: 'Facility category' }),
Timestamps
created_at: z
.string()
.datetime()
.meta({
description: 'Creation timestamp in ISO 8601 format',
examples: ['2024-01-15T10:30:00Z'],
}),
Arrays
participants: z
.array(ParticipantSchema)
.min(1)
.meta({ description: 'List of participants' }),
tags: z
.array(z.string())
.meta({ description: 'Classification tags' }),
Extraction Guidelines
When to extract a sub-schema into a shared module:
| Extract When | Keep Inline When |
|---|
| Used by 2+ schemas | Used by only one schema |
| Has 3+ fields | Has 1-2 simple fields |
| Independently testable | Trivial validation |
| Represents a domain concept | Just a container |
Extract to src/shared/:
export const AddressSchema = z
.strictObject({
street: z.string().meta({ description: 'Street address' }),
city: z.string().meta({ description: 'City name' }),
country: z
.string()
.meta({ description: 'Country code (ISO 3166-1 alpha-2)' }),
postal_code: z.string().meta({ description: 'Postal/ZIP code' }),
})
.meta({ description: 'Physical address' });
export type Address = z.infer<typeof AddressSchema>;
Then use it in type-specific schemas:
import { AddressSchema } from '../shared/address.schema';
const FacilityDataSchema = z.strictObject({
address: AddressSchema.meta({ description: 'Facility address' }),
});
Type Inference
Always infer types from schemas instead of defining them manually:
export type CreditAudit = z.infer<typeof CreditAuditSchema>;
export type CreditAuditData = z.infer<typeof CreditAuditDataSchema>;
export interface CreditAudit {
name: string;
data: { ... };
}
Do / Don't Quick Reference
const Schema = z.strictObject({
field: z.string().meta({ description: 'Field description' }),
});
const Schema = z.object({
field: z.string(),
});
const Extended = Base.safeExtend({
newField: z.string().meta({ description: '...' }),
});
const Extended = Base.extend({ newField: z.string() });
const Schema = z.strictObject({
document_id: z.string().meta({ description: '...' }),
});
const Schema = z.strictObject({
documentId: z.string().meta({ description: }),
});
= z.< >;
{
: ;
}
result = .(input);
(result.).();
( .(input))..();
Naming Conventions Summary
| Concept | Convention | Example |
|---|
| Schema variable | PascalCase + Schema | CreditAuditDataSchema |
| Type export | PascalCase | CreditAuditData |
| Properties | snake_case | document_id, weight_kg |
| File name | kebab-case + suffix | credit-audit.data.schema.ts |
| Enum (user-facing) | Title Case | Recycling Center |
| Enum (technical) | lowercase | pending |
Additional Reference
For more detailed Zod patterns and project-specific conventions, see ZOD_SCHEMA_PATTERNS.md in the project documentation.