pg2zod
v2.2.5
Published
Introspect PostgreSQL databases and generate strict, comprehensive Zod v4 schemas with full type coverage
Downloads
1,079
Maintainers
Readme
pg2zod
Introspect PostgreSQL databases and generate strict, comprehensive Zod v4 schemas
A modern TypeScript package that automatically generates high-quality, strict Zod schemas from your PostgreSQL database schema. Supports all PostgreSQL types including advanced features like enums, composite types, domains, ranges, arrays, and geometric types.
Features
✨ Comprehensive Type Coverage
- All built-in PostgreSQL types (numeric, text, date/time, boolean, JSON, UUID, etc.)
- Custom types: enums, domains, composite types, range types
- Database views (read-only schemas)
- Functions and procedures (with parameter and return type schemas)
- Arrays (including multi-dimensional)
- Geometric types (point, box, circle, polygon, etc.)
- Network types (inet, cidr, macaddr)
- Full-text search types (tsvector, tsquery)
- Bit strings, XML, and more
🔒 Strict & Safe
- Length constraints (
varchar(n)→.max(n)) - Precision/scale validation for numeric types
- Format validations (UUID, IP, MAC addresses, etc.)
- CHECK constraint parsing (comparisons, BETWEEN, IN, ANY/ARRAY, regex)
- Automatic enum generation from CHECK constraints
- NOT NULL awareness
🎯 Smart Code Generation
- Read schemas (reflect actual DB structure)
- Insert schemas (intelligent optional field detection based on defaults/auto-generation)
- Update schemas (all fields optional but maintain validation)
- TypeScript type inference support with
z.infer<> - Database type interface (organizes all types by schema, similar to Supabase)
- Schema-prefixed naming to avoid collisions (e.g.,
PublicUsersSchema) - Optional camelCase conversion
- CHECK constraint parsing and implementation
- Comprehensive comments
🚀 Modern Stack
- ESM-first
- TypeScript with strict mode
- Zod v4 (latest beta)
- CLI + Programmatic API
Installation
npm install pg2zod
# or
pnpm add pg2zod
# or
yarn add pg2zodQuick Start
CLI Usage
# Generate schemas from a local database
# By default includes: tables, views, functions, composite types, and Database interface
pg2zod --database mydb --output src/db/schema.ts
# Use a connection URL
pg2zod --url postgresql://user:pass@localhost:5432/mydb -o schema.ts
# Skip views, routines, or composite types if you don't need them
pg2zod --database mydb --no-views --no-routines --output schema.ts
# Include SECURITY INVOKER functions (only SECURITY DEFINER by default)
pg2zod --database mydb --security-invoker --output schema.ts
# Skip input schemas if you only need read schemas
pg2zod --database mydb --no-input-schemas --output schema.ts
# Use camelCase for field names
pg2zod --database mydb --camel-case -o schema.ts
# Include specific tables only
pg2zod --database mydb --tables users,posts,comments -o schema.ts
# Multiple schemas
pg2zod --database mydb --schemas public,auth,api -o schema.tsProgrammatic API
import {generateZodSchemasString} from 'pg2zod';
const schemas = await generateZodSchemasString(
{
host: 'localhost',
port: 5432,
database: 'mydb',
user: 'postgres',
password: 'password',
},
{
schemas: ['public'],
generateInputSchemas: true,
includeViews: true, // Default: true
includeRoutines: true, // Default: true
includeCompositeTypes: true, // Default: true
includeComments: true,
strictMode: false,
}
);
console.log(schemas);Type Mapping
Built-in Types
| PostgreSQL Type | Zod Schema |
|----------------------------|-----------------------------------------------|
| smallint, integer | z.number().int() |
| bigint | z.bigint() |
| numeric(p,s), decimal | z.number() with precision/scale comment |
| real, double precision | z.number() |
| varchar(n) | z.string().max(n) |
| char(n) | z.string().length(n) |
| text | z.string() |
| boolean | z.boolean() |
| date, timestamp | z.date() |
| time | z.iso.time() |
| interval | z.iso.duration() |
| uuid | z.uuid() |
| json, jsonb | z.record(z.string(), z.unknown()) |
| inet | z.union([z.ipv4(), z.ipv6()]) |
| cidr | z.union([z.cidrv4(), z.cidrv6()]) |
| macaddr | z.mac() |
| point | z.tuple([z.number(), z.number()]) |
| circle | z.object({ center: ..., radius: ... }) |
| polygon | z.array(z.tuple([z.number(), z.number()])) |
| Arrays | z.array(...) (nested for multi-dimensional) |
Custom Types
Enums:
CREATE TYPE status AS ENUM ('pending', 'active', 'inactive');→
export const StatusSchema = z.enum(['pending', 'active', 'inactive']);
export type Status = z.infer<typeof StatusSchema>;Domains:
CREATE DOMAIN email AS TEXT CHECK (VALUE ~ '^[^@]+@[^@]+$');→
export const EmailSchema = z.string().regex(/^[^@]+@[^@]+$/);
export type Email = z.infer<typeof EmailSchema>;Composite Types:
CREATE TYPE address AS (street TEXT, city TEXT, zip VARCHAR(10));→
export const AddressSchema = z.object({
street: z.string(),
city: z.string(),
zip: z.string().max(10),
});
export type Address = z.infer<typeof AddressSchema>;Range Types:
-- int4range, daterange, tstzrange, etc.→
export const Int4rangeSchema = z.tuple([z.number().int().nullable(), z.number().int().nullable()]);
export type Int4range = z.infer<typeof Int4rangeSchema>;Views:
CREATE VIEW user_stats AS
SELECT u.id,
u.username,
COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.username;→
/** View: public.user_stats (read-only) */
export const PublicUserStatsSchema = z.object({
id: z.number().int(),
username: z.string(),
order_count: z.number().int(),
});
export type PublicUserStats = z.infer<typeof PublicUserStatsSchema>;Functions/Procedures:
CREATE FUNCTION get_user_by_id(user_id INTEGER)
RETURNS TABLE
(
id INTEGER,
username VARCHAR,
email VARCHAR
) AS $$
BEGIN
RETURN QUERY SELECT u.id, u.username, u.email FROM users u WHERE u.id = user_id;
END;
$$
LANGUAGE plpgsql SECURITY DEFINER;→
/** FUNCTION: public.get_user_by_id */
export const PublicGetUserByIdParamsSchema = z.object({
/** integer (IN) */
user_id: z.number().int(),
});
export type PublicGetUserByIdParams = z.infer<typeof PublicGetUserByIdParamsSchema>;
/** Returns: record */
export const PublicGetUserByIdReturnSchema = z.array(z.object({
id: z.number().int(),
username: z.string(),
email: z.string(),
}));
export type PublicGetUserByIdReturn = z.infer<typeof PublicGetUserByIdReturnSchema>;Check Constraints
CHECK constraints are automatically parsed and translated to Zod validations:
CREATE TABLE products
(
price NUMERIC CHECK (price > 0),
quantity INTEGER CHECK (quantity >= 0 AND quantity <= 1000),
code VARCHAR(20) CHECK (code ~ '^[A-Z]{3}-\d{4}$'
) ,
status TEXT CHECK (status = ANY (ARRAY['draft', 'published', 'archived']))
);→
export const PublicProductsSchema = z.object({
price: z.number().min(0.00000000000001),
quantity: z.number().int().min(0).max(1000),
code: z.string().regex(/^[A-Z]{3}-\d{4}$/),
status: z.enum(['draft', 'published', 'archived']),
});Supported CHECK constraint patterns:
- Numeric comparisons:
>, <, >=, <= - BETWEEN:
value BETWEEN min AND max - IN/ANY(ARRAY):
value = ANY (ARRAY['a', 'b'])→z.enum(['a', 'b']) - Regex:
value ~ 'pattern'→z.string().regex(/pattern/) - Length:
length(value) >= n→z.string().min(n)
Database Type Interface
pg2zod generates a Database TypeScript interface that organizes all your database types by schema, similar to Supabase's type generator. This provides a structured way to access all your types:
export interface Database {
public: {
Tables: {
users: {
Row: PublicUsers;
Insert: PublicUsersInsert;
Update: PublicUsersUpdate;
Relationships: []; // No foreign keys
};
posts: {
Row: PublicPosts;
Insert: PublicPostsInsert;
Update: PublicPostsUpdate;
Relationships: [
{
foreignKeyName: "posts_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
];
};
};
Views: {
user_stats: {
Row: PublicUserStatsView;
};
};
Functions: {
get_user_by_id: {
Args: PublicGetUserByIdParams;
Returns: PublicGetUserByIdReturn;
};
log_action: {
Args: Record<string, never>; // No parameters
Returns: void; // No return value
};
};
Enums: {
user_role: PublicUserRole;
status: PublicStatus;
};
CompositeTypes: {
address: PublicAddressComposite;
};
};
auth: {
Tables: {
// auth schema tables...
};
};
}Note: All sections (Tables, Views, Functions, Enums, CompositeTypes) are always present in the Database interface, even if empty. Empty sections use [_ in never]: never type.
Usage:
import { Database } from './schema';
// Access table types
type User = Database['public']['Tables']['users']['Row'];
type UserInsert = Database['public']['Tables']['users']['Insert'];
// Access relationships
type PostRelationships = Database['public']['Tables']['posts']['Relationships'];
// Access view types
type UserStats = Database['public']['Views']['user_stats']['Row'];
// Access function types
type GetUserByIdArgs = Database['public']['Functions']['get_user_by_id']['Args'];
type GetUserByIdReturn = Database['public']['Functions']['get_user_by_id']['Returns'];
// Access enums
type UserRole = Database['public']['Enums']['user_role'];CLI Options
Connection Options
--url <url> PostgreSQL connection URL
--host <host> Database host (default: localhost)
--port <port> Database port (default: 5432)
--database <database> Database name (default: postgres)
--user <user> Database user (default: postgres)
--password <password> Database password
--ssl Use SSL connectionGeneration Options
--schemas <schemas> Comma-separated list of schemas (default: public)
--tables <tables> Include only these tables
--exclude-tables <tables> Exclude these tables
--no-input-schemas Skip input schemas (generated by default)
--no-composite-types Skip composite types (generated by default)
--no-views Skip database views (generated by default)
--no-routines Skip functions/procedures (generated by default)
--security-invoker Include SECURITY INVOKER routines (default: DEFINER only)
--branded-types Use branded types for IDs (future)
--strict Fail on unmapped types
--no-comments Don't include comments
--camel-case Convert field names to camelCaseOutput Options
--output <file> Output file path (default: schema.ts)
-o <file> Short form of --outputProgrammatic API
Main Functions
import {
generateZodSchemas,
generateZodSchemasString,
introspectDatabase,
generateSchemas,
formatOutput,
} from 'pg2zod';
// Complete flow: introspect + generate + format
const result = await generateZodSchemas(config, options);
// Get formatted string output
const schemaString = await generateZodSchemasString(config, options);
// Step-by-step
const metadata = await introspectDatabase(config, options);
const result2 = generateSchemas(metadata, options);
const output = formatOutput(result2);Types
interface DatabaseConfig {
host: string;
port: number;
database: string;
user: string;
password: string;
ssl?: boolean | { rejectUnauthorized: boolean };
}
interface SchemaGenerationOptions {
schemas?: string[]; // Default: ['public']
tables?: string[]; // Include only these
excludeTables?: string[]; // Exclude these
generateInputSchemas?: boolean; // Generate Insert/Update schemas (default: true)
includeCompositeTypes?: boolean; // Include composite types (default: true)
includeViews?: boolean; // Include database views (default: true)
includeRoutines?: boolean; // Include functions/procedures (default: true)
includeSecurityInvoker?: boolean; // Include SECURITY INVOKER routines (default: false)
useBrandedTypes?: boolean; // Use branded types (future)
strictMode?: boolean; // Fail on unknown types
includeComments?: boolean; // Include comments (default: true)
useCamelCase?: boolean; // Convert to camelCase
customTypeMappings?: Record<string, string>; // Custom mappings
}Examples
Example Database
-- Create enum
CREATE TYPE user_role AS ENUM ('admin', 'user', 'guest');
-- Create domain
CREATE DOMAIN email AS VARCHAR(255)
CHECK (VALUE ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$');
-- Create table
CREATE TABLE users
(
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email email NOT NULL,
role user_role DEFAULT 'user',
age INTEGER CHECK (age >= 18 AND age <= 120),
tags TEXT[],
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);Generated Output
// Generated by pg2zod
// Do not edit manually
import {z} from 'zod';
// ============================================
// Enums
// ============================================
/** PostgreSQL enum: user_role */
export const PublicUserRoleSchema = z.enum(['admin', 'user', 'guest']);
export type PublicUserRole = z.infer<typeof PublicUserRoleSchema>;
// ============================================
// Domains
// ============================================
/** PostgreSQL domain: email (base: character varying) */
export const PublicEmailSchema = z.string().max(255).regex(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/);
export type PublicEmail = z.infer<typeof PublicEmailSchema>;
// ============================================
// Tables
// ============================================
/** Table: public.users - Read schema */
export const PublicUsersSchema = z.object({
id: z.number().int(),
username: z.string().max(50),
email: PublicEmailSchema,
role: PublicUserRoleSchema,
age: z.number().int().min(18).max(120).nullable(),
tags: z.array(z.string()).nullable(),
metadata: z.record(z.string(), z.unknown()).nullable(),
created_at: z.date(),
});
export type PublicUsers = z.infer<typeof PublicUsersSchema>;
/** Insert schema for users - only auto-generated fields and fields with defaults are optional */
export const PublicUsersInsertSchema = z.object({
id: z.number().int().optional(), // auto-generated: SERIAL/identity
username: z.string().max(50), // required: no default
email: PublicEmailSchema, // required: no default
role: PublicUserRoleSchema.optional(), // optional: has DEFAULT 'user'
age: z.number().int().min(18).max(120).nullable(), // nullable but no default, so required
tags: z.array(z.string()).nullable(), // nullable but no default, so required
metadata: z.record(z.string(), z.unknown()).nullable(), // nullable but no default, so required
created_at: z.date().optional(), // optional: has DEFAULT NOW()
});
export type PublicUsersInsert = z.infer<typeof PublicUsersInsertSchema>;
/** Update schema for users - all fields optional, primary keys excluded, validation preserved */
export const PublicUsersUpdateSchema = z.object({
username: z.string().max(50).optional(),
email: PublicEmailSchema.optional(),
role: PublicUserRoleSchema.optional(),
age: z.number().int().min(18).max(120).optional().nullable(),
tags: z.array(z.string()).optional().nullable(),
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
created_at: z.date().optional(),
});
export type PublicUsersUpdate = z.infer<typeof PublicUsersUpdateSchema>;Environment Variables
Set these to avoid passing credentials via CLI:
export PGHOST=localhost
export PGPORT=5432
export PGDATABASE=mydb
export PGUSER=postgres
export PGPASSWORD=passwordContributing
Contributions welcome! Please open an issue or PR.
License
MIT
Credits
Built with:
