Schema Builder Implementations
Purpose: Overview of PostgreSQL to Zod schema builders
Last Updated: 2024-11-28
This document provides an overview of the schema builder architecture. For detailed implementations with code examples, see Builder Implementations.
Builder Architecture
BaseBuilder
Abstract base class for all schema builders:
abstract class BaseBuilder {
constructor(protected typeCatalog: TypeCatalog) {}
abstract build(type: Type): string;
abstract canHandle(type: Type): boolean;
protected buildTypeSchema(type: Type): string {
// Recursively build schema for any type
const builder = this.findBuilder(type);
return builder.build(type);
}
}
Builder Types
EnumBuilder
Generates z.enum() schemas for PostgreSQL enums.
Handles: Enum types
Output: z.enum([value1, value2, ...])
Example:
CREATE TYPE status AS ENUM ('active', 'inactive');
→ z.enum(['active', 'inactive'])
DomainBuilder
Generates schemas for domain types with constraints.
Handles: Domain types
Output: Base schema with .refine() for constraints
Example:
CREATE DOMAIN email AS text CHECK (VALUE ~ '@');
→ z.string().refine((val) => /@/.test(val))
CompositeBuilder
Generates z.object() schemas for composite types.
Handles: Composite/record types
Output: z.object({ field1: schema1, ... })
Example:
CREATE TYPE user_record AS (id bigint, email text);
→ z.object({ id: z.bigint(), email: z.string() })
ArrayBuilder
Generates z.array() schemas for array types.
Handles: Array types
Output: z.array(elementSchema)
Example:
text[]
→ z.array(z.string())
RangeBuilder
Generates schemas for range types.
Handles: Range types
Output: Object with lower, upper, and inclusivity flags
Example:
daterange
→ z.object({ lower: z.string().date().nullable(), upper: z.string().date().nullable(), lowerInclusive: z.boolean(), upperInclusive: z.boolean() })
Base Type Mapping
Complete Mapping Table
const BASE_TYPE_SCHEMAS: Record<string, string> = {
// Integers
'int2': 'z.number().int()',
'int4': 'z.number().int()',
'int8': 'z.bigint()',
// Floating point
'float4': 'z.number()',
'float8': 'z.number()',
'numeric': 'z.number()',
// Strings
'text': 'z.string()',
'varchar': 'z.string()',
'char': 'z.string()',
// Boolean
'bool': 'z.boolean()',
'boolean': 'z.boolean()',
// Dates and times
'timestamp': 'z.string().datetime()',
'timestamptz': 'z.string().datetime()',
'date': 'z.string().date()',
'time': 'z.string().time()',
// JSON
'json': 'z.unknown()',
'jsonb': 'z.unknown()',
// Binary
'bytea': 'z.instanceof(Buffer)',
// UUID
'uuid': 'z.string().uuid()',
// Network
'inet': 'z.string().ip()',
// Special
'ltree': 'z.string()',
'void': 'z.void()'
};
function getBaseTypeSchema(typeName: string): string {
const schema = BASE_TYPE_SCHEMAS[typeName];
if (!schema) {
throw new CodegenError(`Unknown base type: ${typeName}`);
}
return schema;
}
Advanced Patterns
Circular References
For self-referential types, use z.lazy():
// PostgreSQL
CREATE TYPE node AS (
id bigint,
children node[]
);
// Generated
export const nodeSchema: z.ZodType<Node> = z.lazy(() =>
z.object({
id: z.bigint(),
children: z.array(nodeSchema)
})
);
Custom Validators
Map common domain patterns to Zod validators:
const validators: Record<string, string> = {
'email': 'email()',
'url': 'url()',
'uuid': 'uuid()',
'ip_address': 'ip()'
};
Schema Caching
Cache generated schemas to avoid redundant generation:
class BuilderRegistry {
private schemaCache = new Map<string, string>();
getOrBuildSchema(type: Type): string {
const cached = this.schemaCache.get(type.oid.toString());
if (cached) return cached;
const schema = this.buildSchema(type);
this.schemaCache.set(type.oid.toString(), schema);
return schema;
}
}
Error Handling
Builder Errors
class BuilderError extends Error {
constructor(
public type: Type,
message: string
) {
super(`Error building schema for ${type.name}: ${message}`);
this.name = 'BuilderError';
}
}
Constraint Parsing Errors
class ConstraintParseError extends BuilderError {
constructor(
type: Type,
public constraint: string
) {
super(type, `Could not parse constraint: ${constraint}`);
this.name = 'ConstraintParseError';
}
}
Builder Selection
Selection Process
- Check if type is in base type mapping → use base schema
- Try each builder's
canHandle()method - First builder that returns
truehandles the type - Throw error if no builder can handle the type
Builder Priority
Builders are checked in this order:
- EnumBuilder
- DomainBuilder
- CompositeBuilder
- ArrayBuilder
- RangeBuilder
Schema Generation Process
Step 1: Load Type Catalog
Introspect database and build type catalog with dependencies
Step 2: Topological Sort
Sort types so dependencies are generated before dependents
Step 3: Generate Schemas
For each type in sorted order:
- Find appropriate builder
- Generate Zod schema
- Generate TypeScript type using
z.infer - Write to output file
Step 4: Generate Index
Create index file exporting all schemas and types
Detailed Examples
For complete implementation details and code examples, see:
-
Builder Implementations - Full code examples
- Complete builder implementations
- Constraint conversion examples
- Usage examples
- Error handling
- Unit tests
-
Schema Examples - Generated schema examples
- Enum examples
- Composite examples
- Domain examples
- Array examples
- Range examples
Related Documentation
- Schema Generation - Overview and usage
- Type System - Type mappings
- Introspection - Type discovery
- Function Wrappers - Using generated schemas
These builders convert PostgreSQL type metadata into Zod validation schemas, providing runtime type safety.