Schema Generation Examples
Purpose: Code examples for PostgreSQL to Zod schema generation
Last Updated: 2024-11-28
This document contains practical examples of generated Zod schemas. For concepts and architecture, see Schema Builders.
Enum Examples
Simple Enum
// PostgreSQL
CREATE TYPE status AS ENUM ('active', 'inactive', 'pending');
// Generated
export const statusSchema = z.enum(['active', 'inactive', 'pending']);
export type Status = z.infer<typeof statusSchema>;
// Usage
const status: Status = 'active'; // ✅ Valid
const invalid: Status = 'unknown'; // ❌ Compile error
Enum with Special Characters
// PostgreSQL
CREATE TYPE priority AS ENUM ('high', 'medium', 'low', 'very-high');
// Generated
export const prioritySchema = z.enum(['high', 'medium', 'low', 'very-high']);
export type Priority = z.infer<typeof prioritySchema>;
Using Enum Schema
import { statusSchema } from '@db/types';
// Validate at runtime
const userStatus = statusSchema.parse('active'); // ✅ 'active'
const invalid = statusSchema.parse('unknown'); // ❌ Throws ZodError
// Safe parsing
const result = statusSchema.safeParse(userInput);
if (result.success) {
console.log('Valid status:', result.data);
} else {
console.error('Invalid status:', result.error);
}
Domain Examples
Email Domain
// PostgreSQL
CREATE DOMAIN email AS text
CHECK (VALUE ~ '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
// Generated
export const emailSchema = z.string().refine(
(val) => /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(val),
{ message: 'Invalid email format' }
);
export type Email = z.infer<typeof emailSchema>;
// Usage
const email = emailSchema.parse('user@example.com'); // ✅
const invalid = emailSchema.parse('not-an-email'); // ❌ ZodError
Positive Integer Domain
// PostgreSQL
CREATE DOMAIN positive_int AS integer CHECK (VALUE > 0);
// Generated
export const positiveIntSchema = z.number().int().refine(
(val) => val > 0,
{ message: 'Must be positive' }
);
export type PositiveInt = z.infer<typeof positiveIntSchema>;
// Usage
const age = positiveIntSchema.parse(25); // ✅
const invalid = positiveIntSchema.parse(-5); // ❌ ZodError: Must be positive
URL Domain
// PostgreSQL
CREATE DOMAIN url AS text
CHECK (VALUE ~ '^https?://');
// Generated
export const urlSchema = z.string().refine(
(val) => /^https?:\/\//.test(val),
{ message: 'Must be a valid URL' }
);
// Usage
const website = urlSchema.parse('https://example.com'); // ✅
const invalid = urlSchema.parse('not-a-url'); // ❌ ZodError
Composite Type Examples
Simple Composite
// PostgreSQL
CREATE TYPE user_record AS (
id bigint,
email text,
age integer
);
// Generated
export const userRecordSchema = z.object({
id: z.bigint(),
email: z.string(),
age: z.number().int()
});
export type UserRecord = z.infer<typeof userRecordSchema>;
// Usage
const user: UserRecord = {
id: 1n,
email: 'user@example.com',
age: 25
};
const validated = userRecordSchema.parse(user); // ✅
Composite with Nullable Fields
// PostgreSQL
CREATE TYPE profile AS (
user_id bigint,
bio text,
avatar_url text
);
// Generated
export const profileSchema = z.object({
user_id: z.bigint(),
bio: z.string().nullable(),
avatar_url: z.string().nullable()
});
export type Profile = z.infer<typeof profileSchema>;
// Usage
const profile: Profile = {
user_id: 1n,
bio: null, // ✅ Nullable
avatar_url: 'https://example.com/avatar.jpg'
};
Nested Composite
// PostgreSQL
CREATE TYPE address AS (street text, city text, state text, zip text);
CREATE TYPE user_with_address AS (
id bigint,
name text,
address address
);
// Generated (order matters - dependencies first)
export const addressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string(),
zip: z.string()
});
export type Address = z.infer<typeof addressSchema>;
export const userWithAddressSchema = z.object({
id: z.bigint(),
name: z.string(),
address: addressSchema // References address schema
});
export type UserWithAddress = z.infer<typeof userWithAddressSchema>;
// Usage
const user: UserWithAddress = {
id: 1n,
name: 'John Doe',
address: {
street: '123 Main St',
city: 'Springfield',
state: 'IL',
zip: '62701'
}
};
Array Examples
Simple Arrays
// PostgreSQL: text[]
// Generated
z.array(z.string())
// Usage
const tags = ['typescript', 'postgresql', 'zod'];
const validated = z.array(z.string()).parse(tags); // ✅
// PostgreSQL: integer[]
// Generated
z.array(z.number().int())
// Usage
const scores = [85, 90, 92, 88];
const validated = z.array(z.number().int()).parse(scores); // ✅
// PostgreSQL: bigint[]
// Generated
z.array(z.bigint())
// Usage
const ids = [1n, 2n, 3n, 4n];
const validated = z.array(z.bigint()).parse(ids); // ✅
Nested Arrays
// PostgreSQL: integer[][]
// Generated
z.array(z.array(z.number().int()))
// Usage
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
const validated = z.array(z.array(z.number().int())).parse(matrix); // ✅
// PostgreSQL: text[][][]
// Generated
z.array(z.array(z.array(z.string())))
// Usage
const cube = [
[['a', 'b'], ['c', 'd']],
[['e', 'f'], ['g', 'h']]
];
Arrays of Complex Types
// PostgreSQL: user_record[]
// Generated
z.array(userRecordSchema)
// Usage
const users: UserRecord[] = [
{ id: 1n, email: 'user1@example.com', age: 25 },
{ id: 2n, email: 'user2@example.com', age: 30 }
];
const validated = z.array(userRecordSchema).parse(users); // ✅
// PostgreSQL: status[]
// Generated
z.array(statusSchema)
// Usage
const statuses = ['active', 'inactive', 'pending'];
const validated = z.array(statusSchema).parse(statuses); // ✅
Range Type Examples
Date Range
// PostgreSQL: daterange
// Generated
export const daterangeSchema = z.object({
lower: z.string().date().nullable(),
upper: z.string().date().nullable(),
lowerInclusive: z.boolean(),
upperInclusive: z.boolean()
});
export type Daterange = z.infer<typeof daterangeSchema>;
// Usage
const eventDateRange: Daterange = {
lower: '2024-01-01',
upper: '2024-12-31',
lowerInclusive: true,
upperInclusive: true
};
Integer Range
// PostgreSQL: int4range
// Generated
export const int4rangeSchema = z.object({
lower: z.number().int().nullable(),
upper: z.number().int().nullable(),
lowerInclusive: z.boolean(),
upperInclusive: z.boolean()
});
export type Int4range = z.infer<typeof int4rangeSchema>;
// Usage
const scoreRange: Int4range = {
lower: 0,
upper: 100,
lowerInclusive: true,
upperInclusive: true
};
Timestamp Range
// PostgreSQL: tstzrange
// Generated
export const tstzrangeSchema = z.object({
lower: z.string().datetime().nullable(),
upper: z.string().datetime().nullable(),
lowerInclusive: z.boolean(),
upperInclusive: z.boolean()
});
// Usage
const businessHours = {
lower: '2024-01-15T09:00:00Z',
upper: '2024-01-15T17:00:00Z',
lowerInclusive: true,
upperInclusive: false
};
Advanced Patterns
Circular References
// PostgreSQL
CREATE TYPE tree_node AS (
id bigint,
value text,
children tree_node[]
);
// Generated (with lazy evaluation)
export const treeNodeSchema: z.ZodType<TreeNode> = z.lazy(() =>
z.object({
id: z.bigint(),
value: z.string(),
children: z.array(treeNodeSchema) // Self-reference
})
);
export type TreeNode = z.infer<typeof treeNodeSchema>;
// Usage
const tree: TreeNode = {
id: 1n,
value: 'root',
children: [
{
id: 2n,
value: 'child1',
children: []
},
{
id: 3n,
value: 'child2',
children: [
{
id: 4n,
value: 'grandchild',
children: []
}
]
}
]
};
Schema Composition
// Reuse schemas
const emailSchema = z.string().email();
const phoneSchema = z.string().regex(/^\d{3}-\d{3}-\d{4}$/);
// Compose into larger schema
export const contactInfoSchema = z.object({
email: emailSchema,
phone: phoneSchema.optional(),
preferredContact: z.enum(['email', 'phone'])
});
// Use in multiple places
export const userSchema = z.object({
id: z.bigint(),
name: z.string(),
contact: contactInfoSchema
});
export const vendorSchema = z.object({
id: z.bigint(),
companyName: z.string(),
contact: contactInfoSchema // Reused
});
Union Types
// Multiple possible shapes
export const notificationSchema = z.union([
z.object({
type: z.literal('email'),
to: z.string().email(),
subject: z.string(),
body: z.string()
}),
z.object({
type: z.literal('sms'),
to: z.string(),
message: z.string()
}),
z.object({
type: z.literal('push'),
deviceToken: z.string(),
title: z.string(),
body: z.string()
})
]);
// Usage with type guards
function sendNotification(notification: z.infer<typeof notificationSchema>) {
const validated = notificationSchema.parse(notification);
if (validated.type === 'email') {
// TypeScript knows this has 'to', 'subject', 'body'
sendEmail(validated.to, validated.subject, validated.body);
} else if (validated.type === 'sms') {
// TypeScript knows this has 'to', 'message'
sendSMS(validated.to, validated.message);
} else {
// TypeScript knows this has 'deviceToken', 'title', 'body'
sendPush(validated.deviceToken, validated.title, validated.body);
}
}
Testing Examples
Unit Tests for Schemas
import { describe, it, expect } from 'vitest';
import { statusSchema, userRecordSchema, emailSchema } from '@db/types';
import { ZodError } from 'zod';
describe('statusSchema', () => {
it('accepts valid status values', () => {
expect(() => statusSchema.parse('active')).not.toThrow();
expect(() => statusSchema.parse('inactive')).not.toThrow();
expect(() => statusSchema.parse('pending')).not.toThrow();
});
it('rejects invalid status values', () => {
expect(() => statusSchema.parse('unknown')).toThrow(ZodError);
expect(() => statusSchema.parse('')).toThrow(ZodError);
expect(() => statusSchema.parse(null)).toThrow(ZodError);
});
});
describe('userRecordSchema', () => {
it('validates complete user record', () => {
const user = {
id: 1n,
email: 'user@example.com',
age: 25
};
const result = userRecordSchema.parse(user);
expect(result).toEqual(user);
});
it('rejects incomplete user record', () => {
const incomplete = {
id: 1n,
email: 'user@example.com'
// Missing age
};
expect(() => userRecordSchema.parse(incomplete)).toThrow(ZodError);
});
it('rejects wrong types', () => {
const wrongTypes = {
id: '1', // Should be bigint
email: 'user@example.com',
age: 25
};
expect(() => userRecordSchema.parse(wrongTypes)).toThrow(ZodError);
});
});
describe('emailSchema', () => {
it('accepts valid email addresses', () => {
expect(() => emailSchema.parse('user@example.com')).not.toThrow();
expect(() => emailSchema.parse('first.last@company.co.uk')).not.toThrow();
});
it('rejects invalid email addresses', () => {
expect(() => emailSchema.parse('not-an-email')).toThrow(ZodError);
expect(() => emailSchema.parse('@example.com')).toThrow(ZodError);
expect(() => emailSchema.parse('user@')).toThrow(ZodError);
});
});
Integration Tests
import { withTestClient } from '@/test/utils';
import { createUser, getUser } from '@db/functions';
import { userRecordSchema } from '@db/types';
describe('schema validation in functions', () => {
it('validates input and output', async () => {
await withTestClient(async (client) => {
// Function validates input with schema
const userId = await createUser(client, {
p_email: 'test@example.com',
p_age: 25
});
expect(userId).toBeGreaterThan(0n);
// Function validates output with schema
const user = await getUser(client, { p_id: userId });
// Returned data matches schema
expect(() => userRecordSchema.parse(user)).not.toThrow();
expect(user.email).toBe('test@example.com');
expect(user.age).toBe(25);
});
});
it('throws on invalid input', async () => {
await withTestClient(async (client) => {
// Invalid email format
await expect(
createUser(client, {
p_email: 'not-an-email',
p_age: 25
})
).rejects.toThrow(ZodError);
});
});
});
Related Documentation
- Schema Builders - Architecture and concepts
- Schema Generation - Generation process
- Function Patterns - Using generated functions
- Type System - Type mappings
These examples demonstrate how PostgreSQL types are converted to Zod schemas and used in practice.